/* eslint-disable no-await-in-loop */
/* eslint-disable no-unused-vars */
/* eslint-disable no-use-before-define */
/* eslint-disable no-underscore-dangle */
/**
 * 文件模型
 *
 * Created by snowingsea on 2020/02/13.
 */
const download = require('download');
const fs = require('fs-extra');
const mongoose = require('mongoose');
const path = require('path');
const randomstring = require('randomstring');
const gitUrlParse = require('git-url-parse');
const SimpleGit = require('simple-git/promise');
const requestPromise = require('request-promise');
const request = require('request');
const readline = require('readline');
const svnUltimate = require('node-svn-ultimate');
const EventEmitter = require('events');
const { ObjectId } = require('mongodb');
const TestCase = require('./mongo/test-case');
const Func = require('./mongo/function');
const File = require('./mongo/file');
const FileDiag = require('./mongo/file-diag');
const FileGlobalVariable = require('./mongo/file-global-variable');
const FileLiteral = require('./mongo/file-literal');
const FileTypeSystem = require('./mongo/file-type-system');
const ProjectVersion = require('./mongo/project-version');
const functions = require('./functions');
const gitClient = require('./git/git-client');
const publisher = require('./redis/publisher');
const common = require('./common');
const utility = require('./utility');
const fileElement = require('./file-elements');
const tests = require('./tests');
const codeDefects = require('./code-defects');
const FileMacro = require('./mongo/file-macro');
const DataFlow = require('./mongo/data-flow');
const ProjectVersionsModel = require('./project-versions');
const cacheUtil = require('../cacheUtil');
const logger = require('./logger');
const Excel = require('./export/report/adapter/excel');
const { eventHub, eventKind } = require('./event-hub');
const PhoenixError = require('./phoenix-error');
const tasks = require('./tasks');
const RuntimeCheck = require('./mongo/runtime-check');
const FileComment = require('./mongo/file-comment');
const TestbedFlow = require('./mongo/testbed-flow');
const ProgressIndicator = require('./progress-indicator');
const { clearFilesDefects } = require('./defects');
const operationLogs = require('./operation-logs');

/**
 * 文件状态
 * @type {Object}
 */
const fileStatusKind = {
  deleting: -1, // 正在删除

  initial: 0, // 初始化状态

  downloading: 1, // 正在下载
  downloadFailed: 2, // 下载失败
  downloaded: 3, // 已下载

  cloning: 11, // 正在检出（git）
  cloneFailed: 12, // 检出失败（git）
  cloned: 13, // 检出成功（git）

  extracting: 101, // 正在解压缩
  extractFailed: 102, // 解压缩失败
  extracted: 103, // 解压缩完成

  notCompiled: 201, // 未编译
  compiling: 203, // 正在编译
  compileFailed: 204, // 编译失败
  compiled: 205, // 编译成功
  waiting4compiling: 206, // 待编译
};

/**
 * 文件类型
 * @type {Object}
 */
const fileKind = {
  file: 1,
  directory: 2,
  fileOutsideProject: 3,
};
/**
 * 文件屏蔽状态
 * @type {Object}
 */
const shieldFilesStatus = {
  enable: 1, // 没有屏蔽,可用
  disable: 2, // 屏蔽，不可用
};
/**
 * 仓库类型
 * @type {Object}
 */
const repositoryType = {
  file: 1, // 文件上传
  git: 2, // git检出
  svn: 3, // svn检出
};
const fileUpdateErrorKind = {
  git: {
    repositoryNotFound: 1,
    branchNotFound: 2,
    authenticationFailed: 3,
    otherError: 9,
  },
  svn: {
    repositoryNotFound: 10,
    authenticationFailed: 11,
    otherError: 19,
  },
  remoteFile: {
    downloadFailed: 20,
  },
  uploadFile: {
    extractFailed: 30,
    moveFileFailed: 31,
  },
};

/**
 * 项目版本信息文件名
 * @type {string}
 */
const VERSIONFIELNAME = '.smartrocket';

// 文件注释类型
const fileCommentType = {
  // 函数上方注释
  funcAbove: 1,
  // 行上方注释
  lineAbove: 2,
  // 行结束位置(右边)注释
  lineEnd: 3,
};
// 文件注释类型正则表达式
const fileCommentTypeExp = {
  funcAboveRegularExp: /^(.*)(\s*)@brief\s(.*)/,
  lineAboveRegularExp: /^\s*(.*)\/\*(-)?\s(?!@brief)(.*)\s\*\//,
  lineEndRegularExp: /(.+)\s(.*)\/\*-<\s(.*)\s\*\//,
};
/**
 * 获取格式化的路径名
 * @param {String} filepath 路径字符串
 * @return {String}
 */
function getNormalizePath(filepath) {
  filepath = path.join('.', filepath);
  filepath = path.win32.normalize(filepath);
  filepath = utility.replaceAll(filepath, '\\', '/');
  if (filepath !== '.') {
    filepath = `./${filepath}`;
  }
  return filepath;
}

/**
 * 获取概要json信息
 * @param {File} file 文件信息
 * @param {Array} extensions 后缀名列表
 * @param {Number|Array<Number>>} status 文件筛选状态
 * @returns {Object}
 */
function getSummaryJson(file, extensions = undefined, status = undefined) {
  if (!utility.isArrayType(status)) {
    status = [status];
  }
  status = status.filter((value) => value).map((value) => Number(value));
  const count = common.getCountOfFile(file, extensions);
  const json = {
    id: file._id, // eslint-disable-line no-underscore-dangle
    fileId: file._id, // eslint-disable-line no-underscore-dangle
    parentId: file.directoryId,
    fileName: file.fileName,
    name: file.fileName,
    path: file.path,
    status: file.status,
    kind: file.kind,
    encoding: file.encoding,
  };

  // 覆盖率信息
  if (status.length === 0
    || status.filter(
      (state) => [fileStatusKind.compiled, fileStatusKind.waiting4compiling].indexOf(state) >= 0
    ).length >= 0
  ) {
    if (count.statementTotal) {
      json.statementCoverage = count.statementCovered / count.statementTotal;
    }
    if (count.branchTotal) {
      json.branchCoverage = count.branchCovered / count.branchTotal;
    }
    if (count.conditionTotal) {
      json.conditionCoverage = count.conditionCovered / count.conditionTotal;
    }
    if (count.decisionTotal) {
      json.decisionCoverage = count.decisionCovered / count.decisionTotal;
    }
    if (count.mcdcTotal) {
      json.mcdcCoverage = count.mcdcCovered / count.mcdcTotal;
    }
    if (count.invokeTotal) {
      json.invokeCoverage = count.invokeCovered / count.invokeTotal;
    }
    if (count.invokedFuncTotal) {
      json.invokedFuncCoverage = count.invokedFuncCovered / count.invokedFuncTotal;
    }
  }

  // 文件状态
  if (file.kind === fileKind.directory && count.fileCount) {
    // eslint-disable-next-line max-len
    if (count.compileFailedCount && (status.length === 0 || status.indexOf(fileStatusKind.compileFailed) >= 0)) {
      json.status = fileStatusKind.compileFailed;
      // eslint-disable-next-line max-len
    } else if (count.waiting4compilingCount && (status.length === 0 || status.indexOf(fileStatusKind.waiting4compiling) >= 0)) {
      json.status = fileStatusKind.waiting4compiling;
    } else if (count.compiledCount === count.fileCount) {
      json.status = fileStatusKind.compiled;
    }
  }

  if (file.defectsCount) {
    json.defectsCount = file.defectsCount;
  }

  // 执行通过状态
  if (count.failedCount > 0 || count.succeedCount > 0) {
    json.failed = count.failedCount > 0;
  }

  // 未读状态
  if (count.unreadCount > 0) {
    json.unread = true;
  }
  // 文件总数
  if (file.fileCount) {
    json.fileCount = file.fileCount;
  }
  // 屏蔽文件数
  if (file.shieldFilesCount) {
    json.shieldFilesCount = file.shieldFilesCount;
  }
  return json;
}

/**
 * 获取json信息
 * @param {File} file 文件信息
 * @param {Array} extensions 后缀名列表
 * @param {Number} status 文件筛选状态
 * @returns {Object}
 */
function getJson(file, extensions = undefined, status = undefined) {
  const json = getSummaryJson(file, extensions, status);
  if (file.kind === fileKind.file) {
    json.content = file.content;
  }
  json.repository = file.repository;
  if (file.extra) {
    json.extra = file.extra;
  }
  return json;
}

/**
 * 获取文件信息
 * @param {String} fileId 文件id
 * @return {Promise}
 */
function getFile(fileId) {
  return File.findOne({ _id: mongoose.Types.ObjectId(fileId) });
}

/**
 * 获取文件内容
 * @param {String} fileId 文件id
 * @param {File} file 文件信息
 * @return {Promise}
 */
function getFileContent(fileId, file = null) {
  return new Promise((resolve, reject) => {
    Promise.resolve()
      .then(() => {
        if (file) {
          return Promise.resolve(file);
        }
        return getFile(fileId);
      })
      .then(async (file) => {
        if (!file || file.kind === fileKind.directory) {
          return Promise.resolve('');
        }
        const {
          creatorId,
          projectId,
          versionId,
          path,
        } = file;
        let version = await ProjectVersion.findById(versionId);
        const gitCommitId = version.masterVersionId || versionId;
        // if (version.versionType === ProjectVersionsModel.versionType.staticAnalyze) {
        //   gitCommitId = version.currentCommitId;
        // }
        if (version.versionType !== ProjectVersionsModel.versionType.unitTest) {
          version = await ProjectVersion.findById(version.masterVersionId);
        }
        return gitClient.show(creatorId, projectId, version, gitCommitId, path);
      })
      .then(resolve)
      .catch(reject);
  });
}

async function getFileContentStream(fileId, file = null, decode = false) {
  if (!file) {
    file = await getFile(fileId);
  }
  if (!file || file.kind === fileKind.directory) {
    return null;
  }
  const {
    creatorId,
    projectId,
    versionId,
    path,
  } = file;
  let version = await ProjectVersion.findById(versionId);
  let encoding;
  const { currentCommitId } = version;
  if (decode) {
    encoding = file.encoding || version.encoding || 'utf8';
  }
  if (version.versionType !== ProjectVersionsModel.versionType.unitTest) {
    version = await ProjectVersion.findById(version.masterVersionId);
  }
  return gitClient.show(creatorId, projectId, version, currentCommitId, path, true, encoding);
}

/**
 * 通过文件相对路径获取文件信息
 * @param {String} versionId 项目版本id
 * @param {String} filepath 文件路径
 * @param {Boolean} caseSensitive 是否区分大小写
 * @return {Promise}
 */
function getFileByPath(versionId, filepath, caseSensitive = true) {
  versionId = mongoose.Types.ObjectId(versionId);
  return new Promise((resolve, reject) => {
    Promise.resolve()
      .then(() => {
        if (!caseSensitive) {
          const reg = new RegExp(`^${utility.regExpQuote(getNormalizePath(filepath))}$`, 'i'); // 不区分大小写
          return File.findOne({ versionId, path: reg });
        }
        return File.findOne({ versionId, path: getNormalizePath(filepath) });
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 获取文件分组统计
 * @param {String} versionId 项目版本id
 * @param {String} key 要分组的字段名
 * @param {Array} extensions 后缀列表
 * @return {Promise}
 */
function getGroupCount(versionId, key, { extensions = undefined } = {}) {
  const options = {
    versionId: mongoose.Types.ObjectId(versionId),
    kind: fileKind.file,
  };
  if (extensions) {
    options.extname = { $in: extensions };
  }
  return File.aggregate([{
    $match: options,
  }, {
    $group: {
      _id: `$${key}`,
      count: { $sum: 1 },
    },
  }]);
}

/**
 * 获取文件集合
 * TODO: 优化获取逻辑
 * @param {String} versionId 版本id
 * @param {Array} fileIds 文件id列表
 * @param {Array} exceptFileIds 不计入的文件id列表
 * @param {Array|Number} kinds 文件类型列表
 * @param {Array|String} extensions 文件后缀列表
 * @param {Array|Number} status 文件状态列表
 * @param {Object} opts 其他选项
 * @return {Promise}
 */
function getFiles(
  versionId,
  { fileIds = undefined, exceptFileIds = undefined } = {},
  { kinds = Object.values(fileKind), extensions = undefined, status = undefined } = {},
  opts = {} // eslint-disable-line comma-dangle
) {
  return new Promise((resolve, reject) => {
    let rootIds = fileIds || exceptFileIds;
    if (!rootIds) {
      rootIds = [];
    }
    if (!utility.isArrayType(rootIds)) {
      rootIds = [rootIds];
    }
    if (!utility.isArrayType(kinds)) {
      kinds = [kinds];
    }
    if (extensions && !utility.isArrayType(extensions)) {
      extensions = [extensions];
    }
    if (status && !utility.isArrayType(status)) {
      status = [status];
    }
    const kindSet = new Set(kinds.map((kind) => Number.parseInt(kind.toString(), 10)));

    const options = {
      _id: { $in: rootIds.map((id) => mongoose.Types.ObjectId(id)) },
      versionId: mongoose.Types.ObjectId(versionId),
    };
    if (extensions || status) {
      options.$or = [
        { kind: fileKind.file },
        { kind: fileKind.directory },
      ];
      if (extensions) {
        options.$or[0].extname = { $in: extensions };
      }
      if (status) {
        options.$or[0].status = { $in: status };
      }
    }
    Object.keys(opts).forEach((key) => {
      options[key] = opts[key];
    });
    const results = [];
    File.find(options)
      .then((files) => {
        let directoryIds = [];
        const directoryPaths = [];
        files.forEach((file) => {
          if (kindSet.has(file.kind)) {
            results.push(file);
          }
          if (file.kind === fileKind.directory) {
            directoryIds.push(file._id); // eslint-disable-line no-underscore-dangle
            directoryPaths.push(new RegExp(`^${utility.regExpQuote(file.path.concat('/'))}`));
          }
        });
        function step() {
          if (directoryIds.length === 0) {
            if (fileIds) {
              // 去重
              const fileIdSet = new Set();
              const files = [];
              results.forEach((file) => {
                const fileId = file._id; // eslint-disable-line no-underscore-dangle
                if (fileIdSet.has(fileId.toString())) {
                  return;
                }
                files.push(file);
                fileIdSet.add(fileId.toString());
              });
              resolve(files);
              return;
            }
            const options = {
              versionId,
              kind: { $in: kinds },
              _id: { $nin: results.map(({ _id }) => _id) },
            };
            if (extensions || status) {
              options.$or = [
                { kind: fileKind.file },
                { kind: fileKind.directory },
              ];
              if (extensions) {
                options.$or[0].extname = { $in: extensions };
              }
              if (status) {
                options.$or[0].status = { $in: status };
              }
            }
            Object.keys(opts).forEach((key) => {
              options[key] = opts[key];
            });
            File.find(options).then(resolve).catch(reject);
            return;
          }
          // find files in dir by path
          const options = {
            path: { $in: directoryPaths },
            versionId: mongoose.Types.ObjectId(versionId),
            kind: { $in: kinds },
          };
          if (extensions || status) {
            options.$or = [
              { kind: fileKind.file },
              { kind: fileKind.directory },
            ];
            if (extensions) {
              options.$or[0].extname = { $in: extensions };
            }
            if (status) {
              options.$or[0].status = { $in: status };
            }
          }
          Object.keys(opts).forEach((key) => {
            options[key] = opts[key];
          });
          File.find(options)
            .then((files) => {
              directoryIds = [];
              files.forEach((file) => {
                results.push(file);
              });
              step();
            })
            .catch(reject);
        }
        step();
      })
      .catch(reject);
  });
}

async function getFilesByIds(fileIds, {
  projection = undefined,
} = {}) {
  fileIds = fileIds.map(mongoose.Types.ObjectId);
  const query = File.find({
    _id: { $in: fileIds },
  });
  if (projection) {
    query.projection(projection);
  }
  return query.exec();
}

/**
 * 项目总览- 文件列表
 * @param {String} versionId 版本id
 * @param {Object} param1 options参数
 * @returns {Promise}
 */
async function getOverview(versionId, {
  status,
  extensions,
  page = 1,
  pageSize = 10,
  sortBy = 'sortFilePath',
  order = 'asc',
} = {}) {
  // init sort fields
  let coveredField;
  let totalField;
  let sortField;
  const buildSortField = [{ $set: { sortPathArray: { $split: ['$path', '/'] } } },
    {
      $set: {
        sortFilePath: {
          $reduce: {
            input: '$sortPathArray', initialValue: '', in: { $concat: ['$$value', { $cond: [{ $eq: ['$$value', ''] }, '', '\x00'] }, '$$this'] },
          },
        },
      },
    }];
  switch (sortBy) {
    case 'branchCoverage':
    case 'conditionCoverage':
    case 'decisionCoverage':
    case 'statementCoverage':
    case 'mcdcCoverage':
    case 'invokeCoverage':
    case 'invokedFuncCoverage':
      coveredField = '$'.concat(sortBy.slice(0, -8)).concat('Covered');
      totalField = '$'.concat(sortBy.slice(0, -8)).concat('Total');
      sortField = sortBy;
      buildSortField.push({
        $set: {
          coveredArray: { $cond: { if: { $gt: [coveredField, null] }, then: { $objectToArray: coveredField }, else: [] } },
        },
      },
      { $set: { totalArray: { $cond: { if: { $gt: [totalField, null] }, then: { $objectToArray: totalField }, else: [] } } } },
      { $set: { coveredNum: { $reduce: { input: '$coveredArray', initialValue: 0, in: { $add: ['$$value', '$$this.v'] } } } } },
      { $set: { totalNum: { $reduce: { input: '$totalArray', initialValue: 0, in: { $add: ['$$value', '$$this.v'] } } } } },
      {
        $set: { [sortField]: { $cond: { if: { $gt: ['$totalNum', 0] }, then: { $divide: ['$coveredNum', '$totalNum'] }, else: -1 } } },
      });
      break;
    default:
      break;
  }
  status = status ? [].concat(status).map((s) => parseInt(s, 10)) : undefined;
  extensions = extensions ? [].concat(extensions) : undefined;
  const addArr = [
    {
      versionId: mongoose.Types.ObjectId(versionId),
    },
    {
      kind: fileKind.file,
    },
  ];
  if (status) {
    addArr.push({
      status: { $in: status },
    });
  }
  if (extensions) {
    addArr.push({
      extname: { $in: extensions },
    });
  }
  const collectCoverageFilter = [
    {
      $lookup: {
        from: 'functions',
        let: { fileId: '$_id' },
        pipeline: [
          {
            $match: {
              $expr: {
                $and: [
                  { $eq: ['$fileId', '$$fileId'] },
                  { $eq: ['$collectCoverage', true] },
                  { $in: ['$kind', fileElement.getCountableKinds()] },
                ],
              },
            },
          },
        ],
        as: 'functions',
      },
    },
    {
      $set: {
        collectCoverageFuncsCount: { $size: '$functions' },
      },
    },
    // 已测函数数量
    {
      $set: {
        functionCountTested: {
          $reduce: {
            input: '$functions',
            initialValue: 0,
            in: {
              $add: [{
                $cond: {
                  if: { $or: [{ $gt: ['$$this.failedCount', 0] }, { $gt: ['$$this.succeedCount', 0] }] },
                  then: 1,
                  else: 0,
                },
              }, '$$value'],
            },
          },
        },
      },
    },
    {
      $match: {
        $or: [
          { collectCoverageFuncsCount: { $gt: 0 } },
          { collectCoverage: true },
          { collectCoverage: { $exists: 0 } },
        ],
      },
    },
  ];
  const ret = await File.aggregate([
    {
      $match: {
        $and: addArr,
      },
    },
    ...collectCoverageFilter,
    {
      $count: 'total',
    },
  ]).allowDiskUse(true);
  const [{ total }] = ret && ret.length ? ret : [{ total: 0 }];
  const arr = await File.aggregate([
    {
      $match: {
        $and: addArr,
      },
    },
    ...collectCoverageFilter,
    {
      $group: { _id: '$_id', functionCount: { $sum: '$collectCoverageFuncsCount' }, root: { $first: '$$ROOT' } },
    },
    {
      $replaceRoot: { newRoot: { $mergeObjects: ['$root', { functionCount: '$functionCount' }] } },
    },
    {
      $unwind: { path: '$functions', preserveNullAndEmptyArrays: true },
    },
    {
      $lookup: {
        from: 'test-cases', localField: 'functions._id', foreignField: 'functionId', as: 'tests',
      },
    },
    {
      $unwind: { path: '$tests', preserveNullAndEmptyArrays: true },
    },
    {
      $group: {
        _id: '$_id',
        testCount: { $sum: { $cond: { if: '$tests', then: 1, else: 0 } } },
        // 已测用例数量
        testCountTested: {
          $sum: {
            $cond: [
              { $ne: [{ $ifNull: ['$tests.resultKind', tests.resultKind.unknown] }, 0] },
              1,
              0,
            ],
          },
        },
        root: { $first: '$$ROOT' },
      },
    },
    {
      $replaceRoot: { newRoot: { $mergeObjects: ['$root', { testCount: '$testCount', testCountTested: '$testCountTested' }] } },
    },
    ...buildSortField,
    {
      $sort: {
        [sortBy]: order.toLowerCase() === 'asc' ? 1 : -1,
        sortFilePath: 1,
      },
    },
    {
      $skip: (page - 1) * pageSize,
    },
    {
      $limit: pageSize,
    },
    {
      $project: {
        functionCount: 1,
        functionCountTested: 1,
        testCount: 1,
        testCountTested: 1,
        statementCovered: 1,
        statementTotal: 1,
        branchCovered: 1,
        branchTotal: 1,
        conditionCovered: 1,
        conditionTotal: 1,
        decisionCovered: 1,
        decisionTotal: 1,
        mcdcCovered: 1,
        mcdcTotal: 1,
        status: 1,
        fileName: 1,
        path: 1,
        numberOfLines: 1,
        invokedFuncTotal: 1,
        invokedFuncCovered: 1,
        invokeTotal: 1,
        invokeCovered: 1,
      },
    },
  ]).allowDiskUse(true);

  return [arr.map((item) => {
    const {
      statementCovered,
      statementTotal,
      branchCovered,
      branchTotal,
      conditionCovered,
      conditionTotal,
      decisionCovered,
      decisionTotal,
      mcdcCovered,
      mcdcTotal,
      invokedFuncTotal,
      invokedFuncCovered,
      invokeTotal,
      invokeCovered,
    } = common.getCountOfFile(item, extensions);
    return {
      ...item,
      statementCovered,
      statementTotal,
      branchCovered,
      branchTotal,
      conditionCovered,
      conditionTotal,
      decisionCovered,
      decisionTotal,
      mcdcCovered,
      mcdcTotal,
      invokedFuncTotal,
      invokedFuncCovered,
      invokeTotal,
      invokeCovered,
    };
  }), total];
}

/**
 * 项目总览 - 文件列表xlsx文件
 * @param {String} versionId 版本id
 * @param {Array|String} extensions 文件后缀列表
 * @param {String} coverages 覆盖率类型
 * @param {String} excelName 文件名
 * @param {Object} locale 国际化
 * @returns {Promise}
 */
async function generateOverviewXlsx(versionId, extensions, coverages, excelName, locale) {
  const page = 1;
  const pageSize = 1048575;
  const [fileList, fileTotalNumber] = await getOverview(versionId, { extensions, page, pageSize });

  // coverage info
  const coveragesName = ['invoke', 'invokedFunc', 'statement', 'branch', 'condition', 'decision', 'mcdc'];
  const coveragesArray = [];
  [
    tests.coverageKind.invoke,
    tests.coverageKind.invokedFunc,
    tests.coverageKind.statement,
    tests.coverageKind.branch,
    tests.coverageKind.condition,
    tests.coverageKind.decision,
    tests.coverageKind.mcdc,
  ].forEach((coverageKind, index) => {
    if (coverages & coverageKind) {
      coveragesArray.push(coveragesName[index]);
    }
  });

  // excel cache path
  const outputDir = path.join(cacheUtil.getConfigPath('exportDir'), randomstring.generate(16));
  await fs.ensureDir(outputDir);
  const outPath = path.join(outputDir, excelName);
  const fileExcel = new Excel();
  fileExcel.addSheet('Sheet1');

  // build data
  const columns = [{
    header: locale.__('overview.file.fileName'),
    key: 'fileName',
    width: 16.93,
  }, {
    header: locale.__('overview.file.functionCount'),
    key: 'functionCount',
    width: 16.93,
  }, {
    header: locale.__('overview.file.functionCountTested'),
    key: 'functionCountTested',
    width: 16.93,
  }, {
    header: locale.__('overview.file.testCount'),
    key: 'testCount',
    width: 16.93,
  }, {
    header: locale.__('overview.file.testCountTested'),
    key: 'testCountTested',
    width: 16.93,
  }, {
    header: locale.__('overview.file.numberOfLines'),
    key: 'numberOfLines',
    width: 16.93,
  }];

  const data = [];
  fileList.forEach((file) => {
    const fileData = {
      fileName: file.fileName,
      functionCount: file.functionCount,
      functionCountTested: file.functionCountTested,
      testCount: file.testCount,
      testCountTested: file.testCountTested,
      numberOfLines: file.numberOfLines,
    };
    let covered;
    let total;
    let result;
    let fraction;
    coveragesArray.forEach((name) => {
      covered = name.concat('Covered');
      total = name.concat('Total');
      result = name.concat('Coverage');
      fraction = name.concat('Fraction');
      fileData[fraction] = String(file[covered]).concat('/').concat(String(file[total]));
      fileData[result] = file[total] ? String(Math.round((file[covered] / file[total]) * 10000) / 100).concat('%') : '-';
    });
    fileData.status = locale.__('overview.file.'.concat(utility.getObjectKeyByValue(fileStatusKind, file.status)));
    data.push(fileData);
  });

  coveragesArray.forEach((name) => {
    columns.push({
      header: locale.__('overview.file.'.concat(name).concat('Coverage')),
      key: name.concat('Coverage'),
      width: 16.93,
    });
    columns.push({
      header: locale.__('overview.file.'.concat(name).concat('CoverageRate')),
      key: name.concat('Fraction'),
      width: 16.93,
    });
  });
  columns.push({
    header: locale.__('overview.file.status'),
    key: 'status',
    width: 16.93,
  });
  fileExcel.addRows(columns, data);

  // style
  for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i += 1) {
    for (let j = 1; j <= fileTotalNumber + 1; j += 1) {
      fileExcel.setCell(`${String.fromCharCode(i)}${j}`);
    }
  }

  await fileExcel.writeFile(outPath);
  return [outPath, outputDir];
}

/**
 * 项目总览- 根据文件类型分组统计数量
 * @param {String} versionId 版本id
 * @param {Object} param1 options参数
 * @returns {Promise}
 */
async function fileCountOverview(versionId, {
  extensions,
} = {}) {
  extensions = extensions ? [].concat(extensions) : undefined;
  const allStatusSet = [
    fileStatusKind.notCompiled,
    fileStatusKind.compileFailed,
    fileStatusKind.compiled,
    fileStatusKind.waiting4compiling,
  ];
  const addArr = [
    {
      versionId: mongoose.Types.ObjectId(versionId),
    },
    {
      kind: fileKind.file,
    },
    {
      status: {
        $in: allStatusSet,
      },
    },
  ];
  if (extensions) {
    addArr.push({
      extname: { $in: extensions },
    });
  }
  const ret = await File.aggregate([
    {
      $match: {
        $and: addArr,
      },
    },
    {
      $lookup: {
        from: 'functions',
        let: { fileId: '$_id' },
        pipeline: [
          {
            $match: {
              $expr: {
                $and: [
                  { $eq: ['$fileId', '$$fileId'] },
                  { $eq: ['$collectCoverage', true] },
                  { $in: ['$kind', fileElement.getCountableKinds()] },
                ],
              },
            },
          },
          { $project: { _id: 0 } },
        ],
        as: 'funcs',
      },
    },
    {
      $set: {
        collectCoverageFuncsCount: { $size: '$funcs' },
      },
    },
    {
      $match: {
        $or: [
          { collectCoverageFuncsCount: { $gt: 0 } },
          { collectCoverage: true },
          { collectCoverage: { $exists: 0 } },
        ],
      },
    },
    {
      $group: {
        _id: '$status',
        count: { $sum: 1 },
      },
    },
    {
      $project: {
        status: '$_id',
        count: 1,
        _id: 0,
      },
    },
  ]).allowDiskUse(true);
  return allStatusSet.map((status) => {
    const { count } = ret.find((retItem) => retItem.status === status) || { count: 0 };
    return {
      status,
      count,
    };
  });
}

/**
 * 文件过滤
 * @param {Array} files 文件
 * @param {Object} conditions 传入条件
 * @param Promise
 */
function filterByAdditional(files, conditions) {
  let getTestResults = Promise.resolve(null);
  const testResults = conditions.testResult || null;
  const fileIds = files.filter((file) => file.kind === fileKind.file).map((file) => file._id);
  if (testResults !== null) {
    getTestResults = tests.getTestResultsGroup(
      'fileId',
      { fileIds, resultKinds: testResults },
    );
  }
  let getCodeDefects = Promise.resolve(null);
  const hasCodeDefect = conditions.hasCodeDefect ? Number(conditions.hasCodeDefect) : 0;
  if (hasCodeDefect) {
    getCodeDefects = codeDefects.getCodeDefectsGroup('fileId', { fileIds });
  }
  let getChangedFuncs = Promise.resolve(null);
  const changed = conditions.functionChange ? [].concat(conditions.functionChange) : undefined;
  if (changed) {
    getChangedFuncs = Func.find({
      kind: fileElement.elementKind.function,
      fileId: { $in: fileIds },
      changed: { $in: changed },
    }).then((funcs) => funcs.reduce((obj, func) => {
      const k = func.fileId.toString();
      obj[k] = obj[k] || [];
      obj[k].push(func);
      return obj;
    }, {}));
  }
  let getVersion = Promise.resolve();
  if (files.length > 0) {
    getVersion = ProjectVersionsModel.getVersion(files[0].versionId);
  }
  return Promise.all([getTestResults, getCodeDefects, getChangedFuncs, getVersion])
    .then(([testResults, defects, changedFuncs, version]) => files.filter((file) => {
      const defectLevel = conditions.defectLevel || null;
      if (defectLevel && !(file.defectsCount && file.defectsCount[defectLevel] > 0)) {
        return false;
      }
      const coverage = conditions.coverage || '';
      const count = common.getCountOfFile(file, conditions.extensions);
      if (coverage
        && !(
          // eslint-disable-next-line max-len,no-bitwise
          ((version.coverages & tests.coverageKind.statement) > 0 && count.statementTotal && utility.handleDynamicComparison(coverage, count.statementCovered / count.statementTotal))
          // eslint-disable-next-line max-len,no-bitwise
          || ((version.coverages & tests.coverageKind.branch) > 0 && count.branchTotal && utility.handleDynamicComparison(coverage, count.branchCovered / count.branchTotal))
          // eslint-disable-next-line max-len,no-bitwise
          || ((version.coverages & tests.coverageKind.condition) > 0 && count.conditionTotal && utility.handleDynamicComparison(coverage, count.conditionCovered / count.conditionTotal))
          // eslint-disable-next-line max-len,no-bitwise
          || ((version.coverages & tests.coverageKind.decision) > 0 && count.decisionTotal && utility.handleDynamicComparison(coverage, count.decisionCovered / count.decisionTotal))
          // eslint-disable-next-line max-len,no-bitwise
          || ((version.coverages & tests.coverageKind.mcdc) > 0 && count.mcdcTotal && utility.handleDynamicComparison(coverage, count.mcdcCovered / count.mcdcTotal))
          // eslint-disable-next-line max-len,no-bitwise
          || ((version.coverages & tests.coverageKind.invoke) > 0 && count.invokeTotal && utility.handleDynamicComparison(coverage, count.invokeCovered / count.invokeTotal))
          // eslint-disable-next-line max-len,no-bitwise
          || ((version.coverages & tests.coverageKind.invokedFunc) > 0 && count.invokedFuncTotal && utility.handleDynamicComparison(coverage, count.invokedFuncCovered / count.invokedFuncTotal))
        )
      ) {
        return false;
      }
      if (file.kind === fileKind.file) {
        if (testResults !== null && !testResults[file._id.toString()]) {
          return false;
        }
        if (defects !== null && !defects[file._id.toString()]) {
          return false;
        }

        if (changedFuncs !== null && !changedFuncs[file._id.toString()]) {
          return false;
        }
      }
      return true;
    }));
}

/**
 * 获取子目录/文件列表
 * @param {String} directoryId 父目录文件id
 * @param {Object} conditions 传入条件
 * @return {Promise}
 */
function getChildren(
  directoryId,
  conditions
) {
  let {
    extensions,
    status,
  } = conditions;
  const ignoreEmptyDir = Number(conditions.ignoreEmptyDir);
  return new Promise((resolve, reject) => {
    const options = {
      directoryId: mongoose.Types.ObjectId(directoryId),
    };
    if (status) {
      if (!utility.isArrayType(status)) {
        status = [status];
      }
      status = status.map((item) => Number.parseInt(item.toString(), 10));
    }
    if (extensions) {
      if (!utility.isArrayType(extensions)) {
        extensions = [extensions];
      }
      options.$or = [
        { kind: fileKind.file, extname: { $in: extensions } },
        { kind: fileKind.directory },
      ];
      if (status) {
        options.$or[0].status = { $in: status };
      }
    } else if (status) {
      options.status = { $in: status };
    }
    if (global.config.fileFilters && global.config.fileFilters.length > 0) {
      options.fileName = { $nin: global.config.fileFilters };
    }
    File.find(options).sort({ fileName: 1 })
      .then((files) => filterByAdditional(files, conditions))
      .then((files) => {
        if (ignoreEmptyDir) {
          files = files.filter((file) => {
            if (file.kind === fileKind.file) {
              return true;
            }
            if (extensions && extensions.filter(
              (extension) => file.fileCount && file.fileCount[common.renameMongoKey(extension)] > 0
            ).length === 0) {
              return false;
            }
            return true;
          });
        }
        resolve(files);
      })
      .catch(reject);
  });
}

/**
 * 删除空文件夹
 * @param {File} root 根目录
 */
function removeEmptyDirectoryOfTree(root) {
  if (root.kind === fileKind.file) {
    delete root.children;
    return;
  }
  if (!root.children) {
    root.children = [];
  }
  root.children.forEach((child) => {
    removeEmptyDirectoryOfTree(child);
  });
  root.children = root.children.filter((child) => {
    if (child.kind === fileKind.file) {
      return true;
    }
    return child.children && child.children.length > 0;
  });
}

/**
 * 树结构过滤文件类型
 * @param {File} root 根目录
 * @param {Boolean} keepFile 是否保留文件
 */
function filterFileOfTree(root, keepFile) {
  if (!root.children) {
    return;
  }
  root.children = root.children.filter((child) => (child.kind !== fileKind.file || keepFile));
  root.children.forEach((child) => {
    filterFileOfTree(child, keepFile);
  });
}

/**
 * 获取目录树结构
 * TODO: 效率优化
 * @param {String} rootFileId 根文件id
 * @param {Object} conditions 传入条件
 * @return {Promise}
 */
function getTree(rootFileId, conditions) {
  let {
    kinds = undefined,
    extensions = undefined,
    status = undefined,
  } = conditions;
  const ignoreEmptyDir = conditions.ignoreEmptyDir || 0;
  return new Promise((resolve, reject) => {
    getFile(rootFileId)
      .then((file) => {
        const options = {
          versionId: file.versionId,
          path: new RegExp(`^${utility.regExpQuote(getNormalizePath(file.path))}.?`),
        };
        const fileOptions = { kind: fileKind.file };
        if (extensions) {
          if (!utility.isArrayType(extensions)) {
            extensions = [extensions];
          }
          fileOptions.extname = { $in: extensions };
        }
        if (status) {
          if (!utility.isArrayType(status)) {
            status = [status];
          }
          fileOptions.status = { $in: status.map((item) => Number.parseInt(item.toString(), 10)) };
        }
        if (fileOptions.extname || fileOptions.status) {
          options.$or = [
            fileOptions,
            { kind: fileKind.directory },
          ];
        }
        if (global.config.fileFilters && global.config.fileFilters.length > 0) {
          options.fileName = { $nin: global.config.fileFilters };
        }
        return File.find(options).sort({ fileName: 1 }).lean({ defaults: true });
      })
      .then((files) => filterByAdditional(files, conditions))
      .then((files) => {
        if (files.length === 0) {
          return resolve(null);
        }
        let root;
        const fileMap = new Map();
        files.forEach((file) => {
          fileMap.set(file._id.toString(), file); // eslint-disable-line no-underscore-dangle
        });
        files.forEach((file) => {
          const fileId = file._id.toString(); // eslint-disable-line no-underscore-dangle
          if (rootFileId.toString() === fileId) {
            root = file;
          }
          if (!file.directoryId) {
            return;
          }
          const father = fileMap.get(file.directoryId.toString());
          if (!father) {
            return;
          }
          if (!father.children) {
            father.children = [];
          }
          father.children.push(file);
        });
        if (Number(ignoreEmptyDir)) {
          removeEmptyDirectoryOfTree(root);
        }
        let keepFile = false;
        if (kinds) {
          if (!utility.isArrayType(kinds)) {
            kinds = [kinds];
          }
          kinds.forEach((kind) => {
            if (Number.parseInt(kind.toString(), 10) === fileKind.file) {
              keepFile = true;
            }
          });
        } else {
          keepFile = true;
        }
        filterFileOfTree(root, keepFile);
        return resolve(root);
      })
      .catch(reject);
  });
}

async function getTreeV2(rootFileId, conditions, projectVersion) {
  let {
    extensions = undefined,
    status = undefined,
    testResult = undefined,
    functionChange = undefined,
    alphabetic = undefined,
    hasCodeDefect = undefined,
    ignoreEmptyDir = undefined,
    ignoreFuncs = undefined,
    functionStatus = undefined,
  } = conditions;
  alphabetic = !!Number(alphabetic);
  ignoreEmptyDir = !!Number(ignoreEmptyDir);
  hasCodeDefect = !!Number(hasCodeDefect === 'null' ? 0 : hasCodeDefect);
  functionStatus = Number(functionStatus);
  ignoreFuncs = !!Number(ignoreFuncs);
  const {
    coverage = undefined,
  } = conditions;
  const rootFile = await getFile(rootFileId);
  const options = {
    versionId: rootFile.versionId,
    path: new RegExp(`^${utility.regExpQuote(getNormalizePath(rootFile.path))}.?`),
  };
  const fileOptions = { kind: fileKind.file };
  if (extensions) {
    if (!utility.isArrayType(extensions)) {
      extensions = [extensions];
    }
    fileOptions.extname = { $in: extensions };
  }
  if (status) {
    if (!utility.isArrayType(status)) {
      status = [status];
    }
    fileOptions.status = { $in: status.map((item) => Number.parseInt(item.toString(), 10)) };
  }
  if (fileOptions.extname || fileOptions.status) {
    options.$or = [
      fileOptions,
      { kind: fileKind.directory },
    ];
  }
  if (global.config.fileFilters && global.config.fileFilters.length > 0) {
    options.fileName = { $nin: global.config.fileFilters };
  }
  // 根据文件筛选条件获取所有文件信息
  const availableFiles = await File.find(options).lean();
  const filesMap = {};
  const funcsMap = {};
  const fileIds = [];
  availableFiles.forEach((f) => {
    fileIds.push(f._id);
    // 将所有文件放入map,以便后续处理
    filesMap[f._id.toString()] = f;
  });
  // 根据文件获取所有函数信息
  let funcs = [];
  if (!ignoreFuncs) {
    funcs = await functions.getTreeFuncs(fileIds, hasCodeDefect);
  }
  funcs = funcs.filter((func) => {
    // 将所有函数放入map,以便后续处理
    funcsMap[func._id.toString()] = func;
    // 处理覆盖率统计范围
    fileElement.formatCoverageInfo(func);
    // 计算测试用例总数
    func.testsCount = func.testResults.length;
    // 计算已测用例数
    func.testedTestsCount = 0;
    // 将测试用例结果分组,以便筛选
    const resultGroup = {};
    func.testResults.forEach((result) => {
      if (resultGroup[result.r] === undefined) {
        resultGroup[result.r] = 0;
      }
      resultGroup[result.r] += 1;
      if (result.r !== tests.resultKind.unknown) {
        func.testedTestsCount += 1;
      }
    });
    // 函数状态 已测与未测
    if (functionStatus) {
      if ((functionStatus === 1 && func.testedTestsCount > 0)
        || (functionStatus === 2 && func.testedTestsCount === 0)
      ) {
        return false;
      }
    }
    // 缺陷筛选
    if (hasCodeDefect && !func.hasCodeDefects) {
      return false;
    }
    // 用例结果筛选
    if (testResult) {
      if (!utility.isArrayType(testResult)) {
        testResult = [testResult];
      }
      let valid = false;
      testResult.forEach((kind) => {
        if (resultGroup[Number(kind)]) {
          valid = true;
        }
      });
      if (!valid) {
        return false;
      }
    }
    // 回归函数状态筛选
    if (functionChange) {
      if (!utility.isArrayType(functionChange)) {
        functionChange = [functionChange];
      }
      if (func.kind !== fileElement.elementKind.function || functionChange.indexOf(func.changed) < 0) {
        return false;
      }
    }
    // 覆盖率筛选
    if (coverage && !(
      // eslint-disable-next-line max-len,no-bitwise
      ((projectVersion.coverages & tests.coverageKind.statement) > 0 && func.statementTotal && utility.handleDynamicComparison(coverage, func.statementCovered / func.statementTotal))
      // eslint-disable-next-line max-len,no-bitwise
      || ((projectVersion.coverages & tests.coverageKind.branch) > 0 && func.branchTotal && utility.handleDynamicComparison(coverage, func.branchCovered / func.branchTotal))
      // eslint-disable-next-line max-len,no-bitwise
      || ((projectVersion.coverages & tests.coverageKind.condition) > 0 && func.conditionTotal && utility.handleDynamicComparison(coverage, func.conditionCovered / func.conditionTotal))
      // eslint-disable-next-line max-len,no-bitwise
      || ((projectVersion.coverages & tests.coverageKind.decision) > 0 && func.decisionTotal && utility.handleDynamicComparison(coverage, func.decisionCovered / func.decisionTotal))
      // eslint-disable-next-line max-len,no-bitwise
      || ((projectVersion.coverages & tests.coverageKind.mcdc) > 0 && func.mcdcTotal && utility.handleDynamicComparison(coverage, func.mcdcCovered / func.mcdcTotal))
      // eslint-disable-next-line max-len,no-bitwise
      || ((projectVersion.coverages & tests.coverageKind.invoke) > 0 && func.invokeTotal && utility.handleDynamicComparison(coverage, func.invokeCovered / func.invokeTotal))
      // eslint-disable-next-line max-len,no-bitwise
      || ((projectVersion.coverages & tests.coverageKind.invokedFunc) > 0 && func.invokedFuncTotal && utility.handleDynamicComparison(coverage, func.invokedFuncCovered / func.invokedFuncTotal)))
    ) {
      return false;
    }
    return true;
  });

  const fileTreeElementMap = {};
  const deDuplicateMap = {};
  availableFiles.forEach((file) => {
    const fileId = file._id.toString();
    if (fileTreeElementMap[fileId]) {
      return;
    }
    fileTreeElementMap[fileId] = {
      ...getSummaryJson(file, extensions, status),
      children: [],
    };
    let currentElement = fileTreeElementMap[fileId];
    if (!file.directoryId) {
      return;
    }
    let directoryId = file.directoryId.toString();
    while (directoryId) {
      if (!filesMap[directoryId]) {
        break;
      }
      const dir = filesMap[directoryId];
      if (!fileTreeElementMap[directoryId]) {
        fileTreeElementMap[directoryId] = {
          ...getSummaryJson(file, extensions, status),
          children: [],
        };
      }
      if (!deDuplicateMap[currentElement.id.toString()]) {
        fileTreeElementMap[directoryId].children.push(currentElement);
        deDuplicateMap[currentElement.id.toString()] = 1;
      }
      currentElement = fileTreeElementMap[directoryId];
      directoryId = dir.directoryId ? dir.directoryId.toString() : false;
    }
  });
  funcs.forEach((func) => {
    const funcId = func._id.toString();
    const testableFunc = fileElement.isTestableElement(func.kind);
    if (fileTreeElementMap[funcId]) {
      return;
    }
    fileTreeElementMap[funcId] = {
      ...fileElement.getSummaryJson(func),
      testsCount: func.testsCount || 0,
      testedTestsCount: func.testedTestsCount || 0,
      funcsCount: 0,
      testedFuncsCount: 0,
      children: [],
    };
    if (testableFunc) {
      fileTreeElementMap[funcId].funcsCount = 1;
      fileTreeElementMap[funcId].testedFuncsCount = fileTreeElementMap[funcId].testedTestsCount > 0 ? 1 : 0;
    }
    let currentElement = fileTreeElementMap[funcId];
    let parentId = func.parents.pop();
    while (parentId) {
      parentId = parentId.toString();
      if (!funcsMap[parentId]) {
        break;
      }
      const parent = funcsMap[parentId];
      if (!fileTreeElementMap[parentId]) {
        fileTreeElementMap[parentId] = {
          ...fileElement.getSummaryJson(parent),
          testsCount: 0,
          testedTestsCount: 0,
          funcsCount: 0,
          testedFuncsCount: 0,
          children: [],
        };
      }
      if (!deDuplicateMap[currentElement.id.toString()]) {
        fileTreeElementMap[parentId].children.push(currentElement);
        deDuplicateMap[currentElement.id.toString()] = 1;
      }
      if (testableFunc) {
        fileTreeElementMap[parentId].testsCount += fileTreeElementMap[funcId].testsCount;
        fileTreeElementMap[parentId].testedTestsCount += fileTreeElementMap[funcId].testedTestsCount;
        fileTreeElementMap[parentId].funcsCount += fileTreeElementMap[funcId].funcsCount;
        fileTreeElementMap[parentId].testedFuncsCount += fileTreeElementMap[funcId].testedFuncsCount;
      }
      currentElement = fileTreeElementMap[parentId];
      parentId = func.parents.pop();
    }
    // 顶层函数防止重复拼接到文件的children
    if (deDuplicateMap[`topFunc|${currentElement.id}`]) {
      return;
    }
    deDuplicateMap[`topFunc|${currentElement.id}`] = 1;
    if (fileTreeElementMap[func.fileId.toString()]) {
      fileTreeElementMap[func.fileId.toString()].children.push(currentElement);
    }
  });
  // 计算动态数据和total
  let total = 1;
  let funcFilterEnabled = false;
  if (testResult || functionChange || hasCodeDefect || coverage || functionStatus) {
    funcFilterEnabled = true;
  }
  function step(element) {
    const isFile = element.path && element.kind === fileKind.file;
    const isDir = element.path && element.kind === fileKind.directory;
    const testableFunc = element.functionId && fileElement.isTestableElement(element.kind);
    // 移除一些无用数据
    delete element.funcsCount;
    delete element.testedFuncsCount;
    if (!testableFunc) {
      delete element.testsCount;
      delete element.testedTestsCount;
    }
    if (utility.isArrayType(element.children) && element.children.length > 0) {
      // 对函数子节点进行排序
      if (element.children[0].functionId) {
        if (Number(alphabetic)) {
          element.children = element.children.sort((a, b) => utility.compareStrings(a.fullName, b.fullName));
        } else {
          element.children = element.children.sort((a, b) => a.index - b.index);
        }
      }
      let funcsCount = 0;
      let testedFuncsCount = 0;
      let testsCount = 0;
      let testedTestsCount = 0;
      element.children = element.children.filter((children) => {
        funcsCount += children.funcsCount;
        testedFuncsCount += children.testedFuncsCount;
        testsCount += children.testsCount;
        testedTestsCount += children.testedTestsCount;
        const result = step(children);
        if (!result) {
          return false;
        }
        return true;
      });
      total += element.children.length;
      if (isFile) {
        element.funcsCount = funcsCount;
        element.testedFuncsCount = testedFuncsCount;
        element.testsCount = testsCount;
        element.testedTestsCount = testedTestsCount;
      }
    }
    if (
      // 空文件夹过滤
      (Number(ignoreEmptyDir) && isDir && (!element.children || element.children.length === 0))
      // 函数过滤启用时没有函数的文件过滤
      || (isFile && (!element.children || element.children.length === 0) && funcFilterEnabled)
    ) {
      return false;
    }
    return true;
  }
  step(fileTreeElementMap[projectVersion.rootDirectoryId.toString()]);
  return {
    total,
    tree: fileTreeElementMap[projectVersion.rootDirectoryId.toString()],
  };
}

/**
 * 获取编译信息
 * @param {String} fileId 文件id
 * @return {Promise}
 */
async function getDiag(fileId) {
  const diag = await FileDiag.findOne({ fileId: mongoose.Types.ObjectId(fileId) });
  if (diag && diag.diagMessages) {
    diag.diagMessages = diag.diagMessages.map((diag) => {
      const { includePath } = diag;
      delete diag.includePath;
      // 原始diag信息给的是从错误发生的实际文件位置->当前文件位置，所以需要颠倒过来
      const arr = [diag].concat(includePath || []).reverse();
      const [_diag, ...locations] = arr;
      _diag.type = diag.type;
      _diag.message = diag.message;
      // _diag.description = diag.description;
      // _diag.children = locations;
      _diag.children = [ // 按照前段数据要求，数组仅有两个元素，一个显示位置，一个显示多行详细描述
        {
          name: 'position',
          children: locations,
        },
        {
          name: 'description',
          children: diag.description ? diag.description.split('\n') : [],
        },
      ];
      return _diag;
    });
  }
  return diag;
}
/**
 *  合并文件类型系统
 * @param {String} fileId 文件id
 * @param {String} fileTypes 单文件类型系统
 * @returns
 */
async function getFileTypeSystemByFile(fileId, fileTypes = []) {
  if (fileTypes.length === 0) {
    fileTypes = await FileTypeSystem.find({ fileId }).lean();
  }
  // 类型没有被拆分存储无需合并
  if (fileTypes.length === 1) {
    return fileTypes[0];
  }
  const mergeFileTypes = {
    projectId: '',
    versionId: '',
    fileId: mongoose.Types.ObjectId(fileId),
    types: {},
  };
  const removes = [];
  fileTypes.forEach(({
    _id, projectId, versionId, types,
  }) => {
    if (utility.isEmpty(types)) {
      removes.push(_id);
    } else {
      mergeFileTypes.projectId = projectId;
      mergeFileTypes.versionId = versionId;
      Object.assign(mergeFileTypes.types, types);
    }
  });
  if (removes.length > 0) {
    await FileTypeSystem.deleteMany({ _id: { $in: removes } });
  }
  return mergeFileTypes;
}
/**
 * 获取类型系统信息
 * @param {String|Array} fileId 文件Id
 * @return {Promise}
 */
async function getTypes(fileId) {
  if (utility.isArrayType(fileId)) {
    const fileTypes = await FileTypeSystem.find({ fileId: { $in: fileId.map((id) => mongoose.Types.ObjectId(id)) } }).lean();
    const newFileTypes = new Map();
    fileTypes.forEach(({
      projectId, versionId, fileId, types,
    }) => {
      if (utility.isEmpty(types)) {
        return;
      }
      if (!newFileTypes.has(fileId.toString())) {
        newFileTypes.set(fileId.toString(), {
          projectId,
          versionId,
          fileId,
          types,
        });
        return;
      }
      Object.assign(newFileTypes.get(fileId.toString()).types, types);
    });
    return [...newFileTypes.values()];
  }
  return getFileTypeSystemByFile(fileId);
}

/**
 * 获取文件下的类型名称集合
 * @param {string[]} fileIds
 * @returns {Array<{
 *  fileId: ObjectId,
 *  types: string[]
 * }>}
 */
async function getTypeNames(fileIds) {
  fileIds = [].concat(fileIds);
  const fileTypes = await FileTypeSystem.aggregate([
    { $match: { fileId: { $in: fileIds.map(mongoose.Types.ObjectId) } } },
    {
      $addFields: {
        types: {
          $map: {
            input: { $objectToArray: '$types' },
            as: 'item',
            in: '$$item.k',
          },
        },
      },
    },
    {
      $project: {
        fileId: 1,
        types: 1,
      },
    },
  ]);
  return fileTypes;
}

/**
 * 获取版本内的所有文件的类型系统
 * @param {String} versionId 版本id
 * @param {Array} fileIds 文件id
 */
async function getFileTypeSystemByVersion(versionId, fileIds = undefined) {
  if (!fileIds) {
    fileIds = await File.find({ versionId }, '_id').lean();
  }
  const newvFileTypes = new Map();
  await utility.arrayChunkOperation(fileIds, 50, async ({ _id: fileId }) => {
    const fileTypes = await FileTypeSystem.find({ versionId, fileId }).lean();
    fileTypes.forEach((fileType) => {
      if (utility.isEmpty(fileType.types)) {
        return;
      }
      if (!newvFileTypes.has(fileId.toString())) {
        newvFileTypes.set(fileId.toString(), {
          projectId: fileType.projectId,
          versionId,
          fileId,
          types: fileType.types,
        });
        return;
      }
      Object.assign(newvFileTypes.get(fileId.toString()).types, fileType.types);
    });
  });
  return [...newvFileTypes.values()];
}
/**
 * 获取全局变量
 * @param {String} fileId 文件id
 * @return {Promise}
 */
function getGlobalVariables(fileId) {
  return FileGlobalVariable.findOne({ fileId: mongoose.Types.ObjectId(fileId) });
}

/**
 * 获取字面量
 * @param {String} fileId 文件id
 * @return {Promise}
 */
function getLiterals(fileId) {
  fileId = mongoose.Types.ObjectId(fileId);
  return FileLiteral.findOne({ fileId });
}

function getMacros(fileId) {
  fileId = mongoose.Types.ObjectId(fileId);
  return FileMacro.findOne({ fileId }).lean();
}

async function resetFileOrDirectory(fileId, file = null, parsingTaskId = null) {
  file = file || await getFileById(fileId);
  let children = [];
  const childrenFilter = {
    path: new RegExp(`^${utility.regExpQuote(`${file.path}/`)}`),
    versionId: file.versionId,
  };
  if (file.kind === fileKind.directory) {
    children = await File.find(childrenFilter).lean();
  }
  const arr = [file, ...children];
  const fileIds = arr.map((item) => item._id);
  await fileElement.batchRemoveSourceParsedFiles(file.versionId, fileIds);
  // 以上代码会更新file的覆盖率，导致file的数据过期，所以需要重新取一下
  file = await getFileById(fileId);
  const doc = {
    $set: {
      extra: null,
      status: fileStatusKind.notCompiled,
      failedCount: {},
      succeedCount: {},
      statementCovered: {},
      statementTotal: {},
      branchCovered: {},
      branchTotal: {},
      conditionCovered: {},
      conditionTotal: {},
      decisionCovered: {},
      decisionTotal: {},
      mcdcCovered: {},
      mcdcTotal: {},
      invokeCovered: {},
      invokeTotal: {},
      invokedFuncCovered: {},
      invokedFuncTotal: {},
      compiledCount: {},
      compileFailedCount: {},
      waiting4compilingCount: {},
      unreadCount: {},
      parsingTaskId,
    },
  };
  const publishData = {
    status: fileStatusKind.notCompiled,
    creatorId: file.creatorId,
    projectId: file.projectId,
    versionId: file.versionId,
    testsCount: 0,
    funcsCount: 0,
    testedFuncsCount: 0,
    testedTestsCount: 0,
  };
  const incDoc = { $inc: {} };
  [
    'failedCount',
    'succeedCount',
    'statementCovered',
    'statementTotal',
    'branchCovered',
    'branchTotal',
    'conditionCovered',
    'conditionTotal',
    'decisionCovered',
    'decisionTotal',
    'mcdcCovered',
    'mcdcTotal',
    'invokeCovered',
    'invokeTotal',
    'invokedFuncCovered',
    'invokedFuncTotal',
    'compiledCount',
    'compileFailedCount',
    'waiting4compilingCount',
    'unreadCount',
  ].forEach((key) => {
    if (utility.isEmpty(file[key])) {
      return;
    }
    publishData[key] = {};
    Object.keys(file[key]).forEach((extname) => {
      extname = common.renameMongoKey(extname);
      publishData[key][extname] = 0;
      incDoc.$inc[`${key}.${extname}`] = -file[key][extname];
    });
  });
  await Promise.all([
    File.updateOne({ _id: mongoose.Types.ObjectId(fileId) }, doc),
    common.increaseDirectories(file.directoryId, incDoc),
  ]);
  if (file.kind === fileKind.directory) {
    await File.updateMany(childrenFilter, doc);
  }
  arr.forEach(({ _id: fileId }) => publisher.publishFileMessage('update', {
    ...publishData,
    fileId,
  }));
  const funcs = await Func.find({ fileId: { $in: fileIds } }).lean();
  await eventHub.emit(eventKind.EVENT_FILE_RESET, { fileIds, versionId: file.versionId });
  funcs.forEach((fun) => publisher.publishFunctionMessage(
    'remove',
    {
      functionId: fun._id,
      parentId: fun.parentId,
      fileId: fun.fileId,
      versionId: fun.versionId,
      creatorId: fun.creatorId,
    }
  ));
}

/**
 * 更新依赖文件
 * @param {String} fileId 文件id
 * @param {String} versionId 版本id
 * @param {Object} includeFiles 依赖文件
 * @return {Promise}
 */
function updateFileIncludes(fileId, versionId, includeFiles) {
  return new Promise((resolve, reject) => {
    fileId = mongoose.Types.ObjectId(fileId);
    versionId = mongoose.Types.ObjectId(versionId);
    ProjectVersion.findOne({ _id: versionId })
      .then((version) => {
        const promises = [];
        if (!includeFiles) {
          includeFiles = {};
        }
        Object.keys(includeFiles).forEach((filePath) => {
          promises.push(getFileByPath(versionId, filePath, version.caseSensitive));
        });
        return Promise.all(promises);
      })
      .then((files) => {
        includeFiles = {};
        files.forEach((file) => {
          if (file) {
            includeFiles[file._id] = true; // eslint-disable-line no-underscore-dangle
          }
        });
        return File.updateOne({ _id: fileId }, { includeFiles });
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 更新文件状态
 * @param {String} fileId 文件id
 * @param {Number} status 文件状态
 * @return {Promise}
 */
function updateFileStatus(fileId, status) {
  return new Promise((resolve, reject) => {
    fileId = mongoose.Types.ObjectId(fileId);
    let file;
    getFile(fileId)
      .then((res) => {
        file = res;
        // if (status === fileStatusKind.compileFailed) {
        //   // 编译失败时，先将文件状态重置
        //   return resetFileOrDirectory(fileId, file);
        // }
        return Promise.resolve();
      })
      .then(() => {
        // if (status === fileStatusKind.compileFailed) {
        //   // 编译失败时，由于先重置了文件状态，因此当前文件状态应该为未执行
        //   file.status = fileStatusKind.notCompiled;
        // }
        // 更改状态前 将之前的解析成功和解析失败数据统计更新
        const doc = { $inc: {} };
        doc.$inc.compiledCount = 0;
        doc.$inc.compileFailedCount = 0;
        doc.$inc.waiting4compilingCount = 0;
        switch (file.status) {
          case fileStatusKind.compiled:
            doc.$inc.compiledCount -= 1;
            break;
          case fileStatusKind.compileFailed:
            doc.$inc.compileFailedCount -= 1;
            break;
          case fileStatusKind.waiting4compiling:
            doc.$inc.waiting4compilingCount -= 1;
            break;
          default:
            break;
        }
        switch (status) {
          case fileStatusKind.compiled:
            doc.$inc.compiledCount += 1;
            break;
          case fileStatusKind.compileFailed:
            doc.$inc.compileFailedCount += 1;
            break;
          case fileStatusKind.waiting4compiling:
            if (file.status === fileStatusKind.compiled
              || file.status === fileStatusKind.waiting4compiling
              || file.status === fileStatusKind.compileFailed) {
              doc.$inc.waiting4compilingCount += 1;
            }
            break;
          default:
            break;
        }

        return common.increaseFile(fileId, doc);
      })
      .then(() => {
        const doc = {
          $set: {
            status,
          },
        };
        const condition = {
          _id: fileId,
        };
        const promises = [];
        if (status === fileStatusKind.waiting4compiling) {
          // 只有解析成功的或解析失败的文件可以变成待解析状态
          condition.status = { $in: [fileStatusKind.compileFailed, fileStatusKind.compiled] };
          doc.$set.parsingTaskId = null;

          promises.push(Func.updateMany({ fileId }, { taskId: null }));
          promises.push(TestCase.updateMany({ fileId }, { taskId: null }));
        }
        promises.push(File.updateOne(condition, doc));
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 批量更新该版本下所有文件状态
 * @param {String} projectVersionId 项目版本id
 * @param {Number} status 文件状态
 * @param {Object} options 筛选项
 * @return {Promise}
 */
function updateVersionFilesStatus(projectVersionId, status, options = {}) {
  return new Promise((resolve, reject) => {
    options.versionId = mongoose.Types.ObjectId(projectVersionId);
    File.find(options)
      .then((files) => {
        const promises = files.map((file) => {
          const fileId = file._id; // eslint-disable-line no-underscore-dangle
          return updateFileStatus(fileId, status);
        });
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 添加文件
 * @param {String} creatorId 创建者id
 * @param {String} projectId 项目id
 * @param {String} versionId 项目版本id
 * @param {String} filePath 当前文件路径（相对路径）
 * @param {String} directoryId 父目录文件id
 * @return {Promise}
 */
function addFile(creatorId, projectId, versionId, filePath, directoryId) {
  return new Promise((resolve, reject) => {
    filePath = getNormalizePath(filePath);
    const doc = {
      creatorId: mongoose.Types.ObjectId(creatorId),
      projectId: mongoose.Types.ObjectId(projectId),
      versionId: mongoose.Types.ObjectId(versionId),
      fileName: path.basename(filePath),
      path: filePath,
      extname: path.extname(filePath),
      directoryId,
      status: fileStatusKind.notCompiled,
      kind: fileKind.file,
      fileCount: {},
    };
    const extnameKey = common.renameMongoKey(doc.extname);
    doc.fileCount[extnameKey] = 1;
    const file = new File(doc);
    file.save()
      .then((file) => {
        const data = getJson(file);
        data.creatorId = file.creatorId;
        data.projectId = file.projectId;
        data.versionId = file.versionId;
        publisher.publishFileMessage('add', data);
        resolve(file);
      })
      .catch(reject);
  });
}

/**
 * 添加目录文件
 * @param {String} creatorId 创建者id
 * @param {String} projectId 项目id
 * @param {String} versionId 项目版本id
 * @param {String} dirPath 当前目录路径（相对路径）
 * @param {String} directoryId 父目录文件id
 * @return {Promise}
 */
function addDirectory(creatorId, projectId, versionId, dirPath, directoryId = undefined) {
  return new Promise((resolve, reject) => {
    dirPath = getNormalizePath(dirPath);
    const file = new File({
      creatorId: mongoose.Types.ObjectId(creatorId),
      projectId: mongoose.Types.ObjectId(projectId),
      versionId: mongoose.Types.ObjectId(versionId),
      fileName: path.basename(dirPath),
      path: dirPath,
      directoryId,
      kind: fileKind.directory,
      status: fileStatusKind.notCompiled,
    });
    file.save()
      .then((file) => {
        const data = getJson(file);
        data.creatorId = file.creatorId;
        data.projectId = file.projectId;
        data.versionId = file.versionId;
        if (directoryId) {
          publisher.publishFileMessage('add', data);
        } else {
          publisher.publishRootDirectoryMessage('add', data);
        }
        resolve(file);
      })
      .catch(reject);
  });
}

/**
 * 删除某个项目版本下文件的编译结果
 * @param {String} versionId 项目版本id
 * @return {Promise}
 */
function removeResultOfCompilingInVersion(versionId) {
  const doc = {
    status: fileStatusKind.notCompiled,
    failedCount: {},
    succeedCount: {},
    statementCovered: {},
    statementTotal: {},
    branchCovered: {},
    branchTotal: {},
    conditionCovered: {},
    conditionTotal: {},
    decisionCovered: {},
    decisionTotal: {},
    mcdcCovered: {},
    mcdcTotal: {},
    invokeCovered: {},
    invokeTotal: {},
    invokedFuncCovered: {},
    invokedFuncTotal: {},
    compiledCount: {},
    compileFailedCount: {},
    waiting4compilingCount: {},
    unreadCount: {},
  };
  return Promise.all([
    File.updateMany({ versionId }, { $set: doc }),
    eventHub.emit(eventKind.EVENT_FILE_RESET, { versionId }),
  ]);
}

/**
 * 设置受影响文件为待解析状态
 * @param {String} fileId 文件id
 * @return {Promise}
 */
function updateWaiting4compilingFiles(fileId) {
  return new Promise((resolve, reject) => {
    fileId = mongoose.Types.ObjectId(fileId);
    getFile(fileId)
      .then((res) => {
        const includeFileKey = `includeFiles.${fileId}`;
        const option = { versionId: res.versionId };
        option[includeFileKey] = { $exists: true };
        return File.find(option);
      })
      .then((files) => {
        files.push(fileId);
        const promises = files.map((file) => {
          const fileId = file._id; // eslint-disable-line no-underscore-dangle
          return updateFileStatus(fileId, fileStatusKind.waiting4compiling);
        });
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 更新文件信息，若不存在，创建之，若存在，更新之
 * @param {ProjectVersion} version 项目版本
 * @param {String} root 根目录
 * @param {String} relativePath 相对文件路径
 * @param {String} directoryId 父目录文件id
 * @return {Promise}
 */
function replaceFile(version, root, relativePath, directoryId = undefined) {
  return new Promise((resolve, reject) => {
    const versionId = version._id; // eslint-disable-line no-underscore-dangle
    const { creatorId, projectId } = version;
    relativePath = getNormalizePath(relativePath);
    const filePath = path.join(root, relativePath);
    let kind;
    let isNew = false;
    fs.stat(filePath)
      .then((stats) => {
        kind = stats.isDirectory() ? fileKind.directory : fileKind.file;
        return getFileByPath(versionId, relativePath);
      })
      .then((file) => {
        if (!file) {
          isNew = true;
          if (kind === fileKind.directory) {
            return addDirectory(creatorId, projectId, versionId, relativePath, directoryId);
          }
          return addFile(creatorId, projectId, versionId, relativePath, directoryId);
        }
        if (kind === fileKind.directory) {
          return Promise.resolve(file);
        }
        return updateEncoding(file._id)
          .then(() => updateWaiting4compilingFiles(file._id))
          .then(() => clearFilesDefects(versionId, file._id))
          .then(() => getFile(file._id));
      })
      .then((file) => {
        file.isNew = isNew;
        resolve(file);
      })
      .catch(reject);
  });
}

function increaseDirectories(directoryId, incDoc) {
  return common.increaseDirectoriesMerge(directoryId, incDoc);
}

/**
 * 通过树结构更新文件
 * @param {ProjectVersion} version 项目版本
 * @param {String} root 根目录
 * @param {Map} childrenMap 树字典
 * @param {String} relativePath 相对文件路径
 * @param {String} directoryId 父目录文件id
 * @param {Map} newFiles 新增的文件
 * @param {ProgressIndicator} progressIndicator 处理的进度
 * @return {Promise}
 */
async function updateTreeFiles(version, rootPath, childrenMap, relativePath = '.', directoryId = undefined, newFiles, progressIndicator = new ProgressIndicator()) {
  const versionId = version._id;
  const absPath = path.join(rootPath, relativePath);
  const needDelete = !(await fs.pathExists(absPath));
  // an existed file or directory need to delete
  if (needDelete) {
    return removeSubTree(versionId, relativePath, progressIndicator);
  }
  const exists = await File.findOne({ versionId, path: relativePath });
  if (!exists) {
    // new directory or file
    const inserts = await addSubTree(versionId, directoryId, relativePath, childrenMap, rootPath, progressIndicator);
    return inserts.forEach((file) => newFiles.set(file._id, true));
  }
  const childrenPathArr = [...childrenMap.keys()];
  // an existed directory
  if (childrenPathArr && childrenPathArr.length) {
    const totalLeafCnt = utility.getTreeLeafCnt(childrenMap);
    const childrenLeafCountArr = childrenPathArr.map((childPth) => utility.getTreeLeafCnt(childrenMap.get(childPth)));
    const progressIndicators = progressIndicator.split(childrenLeafCountArr, totalLeafCnt);
    return Promise.all(childrenPathArr
      .map((childPath, idx) => updateTreeFiles(
        version, rootPath, childrenMap.get(childPath), childPath, exists._id, newFiles, progressIndicators[idx]
      )));
  }
  // an existed single file that is modified
  const file = await replaceFile(version, rootPath, relativePath, directoryId);
  await computeNumberOfLines(file._id, path.join(rootPath, relativePath));
  return progressIndicator.finish();
}

/**
 * 更新版本中的文件信息
 * @param {User} creator 创建者
 * @param {Project} project 项目
 * @param {ProjectVersion} version 项目版本
 * @param {boolean} progressNotify 是否需要获取进度信息
 * @return {Promise}
 */
async function updateVersionFiles(creator, project, version, progressNotify) {
  const versionId = version._id; // eslint-disable-line no-underscore-dangle
  const repoPath = gitClient.getLocalRepoPath(creator, project, version);
  const relationVersions = [];
  const [rawVersion, resVersions] = await Promise.all([
    // multiple person uploading files for one version at the same time, the lastCommitId of version info for the last request may out of date
    // so need to retrieve it again before using it.
    ProjectVersionsModel.getVersion(versionId),
    ProjectVersionsModel.getRelationVersions(versionId),
  ]);
  version = rawVersion;
  const newCommitId = await gitClient.getLatestCommitId(creator, project, version);
  relationVersions.push(version);
  // 当前版本非静态分析版本时去掉关联版本中的静态分析版本
  // 更新主版本和集成测试版本文件不影响静态分析版本文件
  resVersions.forEach((resVersion) => {
    if (resVersion.rootDirectoryId) {
      relationVersions.push(resVersion);
    }
  });
  const filesDiffMap = new Map();
  // 统计每个版本对应最新上传的文件的差异
  await Promise.all(relationVersions.map(async (relationVersion) => {
    const { files: leafs } = await gitClient.diffSummary(creator, project, version, relationVersion.currentCommitId, newCommitId);
    // 构建文件树结构
    const treeMap = new Map();
    leafs.forEach(({ file }) => {
      file = getNormalizePath(file);
      if (!treeMap.has(file)) {
        treeMap.set(file, new Map());
      }
      do {
        const dir = path.dirname(file);
        if (!treeMap.has(dir)) {
          treeMap.set(dir, new Map());
        }
        treeMap.get(dir).set(file, treeMap.get(file));
        file = dir;
      } while (file !== '.');
    });
    filesDiffMap.set(relationVersion._id.toString(), treeMap);
  }));
  await Promise.all(relationVersions.map(async (relationVersion) => {
    const newFiles = new Map();
    const treeMap = filesDiffMap.get(relationVersion._id.toString());
    if (treeMap.size === 0) {
      return Promise.resolve();
    }
    let progressIndicator;
    if (progressNotify) {
      progressIndicator = new ProgressIndicator();
      progressIndicator.on('progress', (progress) => {
        const chan = utility.getSessionId();
        if (chan) {
          eventHub.emit(chan, 'database', progress);
        }
      });
    }
    await updateTreeFiles(relationVersion, repoPath, treeMap.get('.'), '.', undefined, newFiles, progressIndicator);
    if (newFiles.size !== 0) {
      await updateVersionFilesStatus(
        relationVersion._id,
        fileStatusKind.waiting4compiling,
        { status: fileStatusKind.compileFailed, kind: fileKind.file }
      );
    }
    await ProjectVersion.updateOne(
      { _id: relationVersion._id },
      { currentCommitId: newCommitId }
    );
    return Promise.resolve();
  }));
}
/**
 * 插入整棵sub tree到数据库
 * @param {String} versionId version id
 * @param {String} parentId 父id
 * @param {String} currentPath sub tree 根节点路径
 * @param {Map} tree sub tree map
 * @param {String} repoPath 仓库路径
 * @param {ProgressIndicator} processed 处理的进度
 * @return {Promise}
 */
async function addSubTree(versionId, parentId, currentPath, tree, repoPath, processed = new ProgressIndicator()) {
  versionId = mongoose.Types.ObjectId(versionId);
  const version = await ProjectVersion.findById(versionId);
  const { creatorId, projectId } = version;

  const inserts = [];
  function genObjectIdForTree(currentPath, tree, directoryId, trace = []) {
    const filePath = currentPath;
    const objId = mongoose.Types.ObjectId();
    const childrenPathArr = [...tree.keys()];
    if (childrenPathArr && childrenPathArr.length) {
      // directory
      const insertObj = {
        _id: objId,
        creatorId,
        projectId,
        versionId,
        fileName: path.basename(filePath),
        path: filePath,
        directoryId,
        kind: fileKind.directory,
        status: fileStatusKind.notCompiled,
        fileCount: {},
      };
      inserts.push(insertObj);
      childrenPathArr.forEach(
        (childPath) => genObjectIdForTree(childPath, tree.get(childPath), objId, [...trace, insertObj])
      );
    } else {
      // file
      const insertObj = {
        _id: objId,
        creatorId,
        projectId,
        versionId,
        fileName: path.basename(filePath),
        path: filePath,
        extname: path.extname(filePath),
        directoryId,
        status: fileStatusKind.notCompiled,
        kind: fileKind.file,
        fileCount: {},
      };
      // 当前版本只收集部分文件的覆盖率情况下，新导入的文件默认不收集覆盖率
      if (version.collectCoverageKind && version.collectCoverageKind === ProjectVersionsModel.collectCoverageKind.partial) {
        insertObj.collectCoverage = false;
      }
      const extnameKey = common.renameMongoKey(insertObj.extname);
      let fileCount = 1;
      if (global.config.fileFilters && global.config.fileFilters.indexOf(insertObj.fileName) >= 0) {
        fileCount = 0;
      }
      insertObj.fileCount[extnameKey] = fileCount;
      inserts.push(insertObj);
      // increaseDirectory
      trace.forEach((obj) => {
        obj.fileCount[extnameKey] = obj.fileCount[extnameKey] || 0;
        obj.fileCount[extnameKey] += fileCount;
      });
    }
  }

  genObjectIdForTree(currentPath, tree, parentId);

  // 分批处理
  const batchSize = 100;
  for (let i = 0; i < inserts.length; i += batchSize) {
    const arr = inserts.slice(i, i + batchSize);
    let leafsCnt = 0; // 用于处理进度
    // eslint-disable-next-line no-restricted-syntax
    for (const obj of arr) {
      if (obj.kind === fileKind.file) {
        leafsCnt += 1;
      }
    }

    // 计算文件行数
    await utility.arrayChunkOperation(arr, 10, async (item) => {
      if (item.kind === fileKind.file) {
        const {
          total, source, empty, comment,
        } = await computeFileCodeLines(path.join(repoPath, item.path));
        Object.assign(item, {
          numberOfLines: total,
          sourceLines: source,
          commentLines: comment,
          emptyLines: empty,
        });
      }
    });

    await File.collection.insertMany(arr);
    processed.advance(arr.length, inserts.length);
  }

  // increase directorys fileCount
  const subRoot = inserts[0];
  if (subRoot.directoryId) {
    const inc = Object.keys(subRoot.fileCount).reduce((inc, extName) => {
      inc[`fileCount.${extName}`] = subRoot.fileCount[extName];
      return inc;
    }, {});
    await increaseDirectories(subRoot.directoryId, { $inc: inc });
  }
  return inserts;
}

async function removeSubTree(versionId, subRootPath, processed = new ProgressIndicator()) {
  versionId = mongoose.Types.ObjectId(versionId);
  const path = getNormalizePath(subRootPath);
  let file = await File.findOne({
    versionId,
    path,
  });
  if (!file) {
    throw new Error(`${subRootPath} not found in database`);
  }

  const childrenFilterRegx = new RegExp(`^${utility.regExpQuote(`${subRootPath}/`)}`);

  let leafIdArr = [];
  if (file.kind === fileKind.file) {
    leafIdArr.push(file._id);
  } else {
    const childrenArr = await File.find({
      versionId,
      path: childrenFilterRegx,
      kind: fileKind.file,
    }, { _id: 1 });
    leafIdArr = leafIdArr.concat(childrenArr.map((c) => c._id));
  }
  await fileElement.batchRemoveSourceParsedFiles(versionId, leafIdArr);
  // batchRemoveSourceParsedFiles may change the coverage, need to reload file info.
  file = await getFileById(file._id);
  // 分批处理
  const batchSize = 100;
  for (let i = 0; i < leafIdArr.length; i += batchSize) {
    const idArr = leafIdArr.slice(i, i + batchSize);
    // delete files
    await File.deleteMany({
      versionId,
      _id: { $in: idArr },
    });
    // delete relate content from other tables
    await eventHub.emit(eventKind.EVENT_FILE_REMOVE, { fileIds: idArr, versionId });
    if (i + batchSize < leafIdArr.length) {
      processed.advance(idArr.length, leafIdArr.length);
    }
  }

  if (file.kind === fileKind.directory) {
    // delete directorys
    await File.deleteOne({
      _id: file._id,
    });
    await File.deleteMany({
      versionId,
      path: childrenFilterRegx,
    });
  }
  // updateWaiting4compilingFiles
  const option = { versionId };
  option.$or = leafIdArr.map((id) => ({
    [`includeFiles.${id}`]: { $exists: true },
  }));
  const includeFiles = await File.find(option, '_id');
  await Promise.all(includeFiles.map(
    (f) => updateFileStatus(f._id, fileStatusKind.waiting4compiling)
  ));
  // increase directorys
  const incDoc = { $inc: {} };
  [
    'failedCount',
    'succeedCount',
    'statementCovered',
    'statementTotal',
    'branchCovered',
    'branchTotal',
    'conditionCovered',
    'conditionTotal',
    'decisionCovered',
    'decisionTotal',
    'mcdcCovered',
    'mcdcTotal',
    'invokedFuncTotal',
    'invokedFuncCovered',
    'invokeCovered',
    'invokeTotal',
    'fileCount',
    'compileFailedCount',
    'compiledCount',
    'waiting4compilingCount',
    'unreadCount',
    'defectsCount',
  ].forEach((key) => {
    if (!file[key]) {
      return;
    }
    Object.keys(file[key]).forEach((extname) => {
      incDoc.$inc[`${key}.${extname}`] = -file[key][extname];
    });
  });
  await increaseDirectories(file.directoryId, incDoc);
  processed.finish();
  // publish delete message
  const data = {
    fileId: file._id, // eslint-disable-line no-underscore-dangle
    creatorId: file.creatorId,
    projectId: file.projectId,
    versionId: file.versionId,
  };
  publisher.publishFileMessage('delete', data);
}

async function cloneFilesFromMasterVersion(masterVersionId, subVersionId) {
  const files = await File.find({ versionId: mongoose.Types.ObjectId(masterVersionId) },
    [
      '_id', 'creatorId', 'projectId', 'fileName', 'path',
      'extname', 'directoryId', 'kind',
      'repository', 'fileCount', 'numberOfLines', 'encoding',
      'sourceLines', 'commentLines', 'emptyLines',
    ],
    { lean: true });
  const oldNewIdMap = new Map();
  let rootDirectoryId;
  files.forEach((file) => {
    if (!oldNewIdMap.has(file._id.toString())) {
      oldNewIdMap.set(file._id.toString(), mongoose.Types.ObjectId());
    }
    file._id = oldNewIdMap.get(file._id.toString());
    if (file.path === '.') {
      rootDirectoryId = file._id;
    }
    if (file.directoryId) {
      if (!oldNewIdMap.has(file.directoryId.toString())) {
        oldNewIdMap.set(file.directoryId.toString(), mongoose.Types.ObjectId());
      }
      file.directoryId = oldNewIdMap.get(file.directoryId.toString());
    }
    file.status = fileStatusKind.notCompiled;
    file.versionId = mongoose.Types.ObjectId(subVersionId);
  });
  if (!rootDirectoryId) {
    throw new Error('数据错乱');
  }
  await File.collection.insertMany(files);
  return rootDirectoryId;
}

/**
 * 向某个目录中添加文件
 * @param {String} directoryId 当前目录文件id
 * @param {Number} kind 文件类型
 * @param {String} fileName 文件名
 * @param {String} content 文件内容
 * @param {File} directory 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function addFile2Directory(
  directoryId,
  { kind, fileName, content = null },
  directory = null,
  version = null,
  project = null,
  creator = null, // eslint-disable-line comma-dangle
) {
  return new Promise((resolve, reject) => {
    kind = Number.parseInt(kind.toString(), 10);
    directoryId = mongoose.Types.ObjectId(directoryId);
    let versionId;
    let repoPath;
    let dirPath;
    let filePath;
    let newDirPath;
    let file;
    common.complementByFileId(directoryId, directory, version, project, creator)
      .then((res) => {
        directory = res.file;
        version = res.version;
        project = res.project;
        creator = res.creator;
        versionId = version._id;
        return gitClient.checkoutBranch(creator, project, version);
      })
      .then(() => {
        repoPath = gitClient.getLocalRepoPath(creator, project, version);
        dirPath = path.normalize(directory.path);
        filePath = path.join(dirPath, fileName);
        const absoluteFilePath = path.join(repoPath, filePath);
        if (kind === fileKind.file) {
          if (content) {
            return utility.writeFile(content, absoluteFilePath);
          }
          return fs.ensureFile(absoluteFilePath);
        }
        return fs.ensureDir(absoluteFilePath);
      })
      .then(() => {
        newDirPath = filePath;
        while (path.dirname(newDirPath) !== dirPath) {
          newDirPath = path.dirname(newDirPath);
        }
        return gitClient.addAllFilesInDirectory(newDirPath, creator, project, version);
      })
      .then(() => updateVersionFiles(creator, project, version))
      .then(() => getFileByPath(versionId, filePath))
      .then((res) => {
        resolve(res);
      })
      .catch(reject);
  });
}

/**
 * 向某个目录中添加文件
 * @param {String} directoryId 当前目录文件id
 * @param {Array} files 文件集合
 * @param {File} directory 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function addFiles(
  directoryId,
  files,
  directory = null,
  version = null,
  project = null,
  creator = null,
  zipFileNameEncoding = null
) {
  return new Promise((resolve, reject) => {
    directoryId = mongoose.Types.ObjectId(directoryId);
    common.complementByFileId(directoryId, directory, version, project, creator)
      .then((res) => {
        directory = res.file;
        version = res.version;
        project = res.project;
        creator = res.creator;
        return gitClient.checkoutBranch(creator, project, version);
      })
      .then(() => {
        const repoPath = gitClient.getLocalRepoPath(creator, project, version);
        const promises = files.map(async (file) => {
          const filePath = path.join(cacheUtil.getConfigPath('uploadDir'), file.filename);
          if (path.extname(file.originalname) === '.zip') {
            try {
              await utility.uncompress(filePath, path.join(repoPath, directory.path), zipFileNameEncoding);
            } catch (error) {
              throw new PhoenixError(`zip file:${file.filename} extract failed error info ${error}`);
            }
          } else {
            await fs.move(filePath, path.join(repoPath, directory.path, file.originalname));
          }
        });
        return Promise.all(promises);
      })
      .then(() => gitClient.addAllFilesInDirectory(directory.path, creator, project, version))
      .then(() => updateVersionFiles(creator, project, version, true))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 将文件夹下所有文件移动至仓库
 * @param {String} dirPath 源文件夹
 * @param {File} file 要移动到的目录路径
 * @param {Object} version 项目版本配置信息
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function moveDirectory2Repo(dirPath, file, version, project, creator) {
  return new Promise((resolve, reject) => {
    const localRepoPath = gitClient.getLocalRepoPath(creator, project, version);
    gitClient.emptyRepoDirectory(creator, project, file.path, null, version)
      .then(() => gitClient.removeGitDirectory(dirPath))
      .then(() => fs.readdir(dirPath))
      .then((files) => {
        if (files.length === 1 && /^[0-9a-fA-F]{24}$/.test(files[0])) {
          dirPath = path.join(dirPath, files[0]);
          return fs.readdir(dirPath);
        }
        return Promise.resolve(files);
      })
      .then((files) => {
        const promises = files.map((filePath) => {
          const sourcePath = path.join(dirPath, filePath);
          const destPath = path.join(localRepoPath, file.path, filePath);
          return fs.move(sourcePath, destPath);
        });
        return Promise.all(promises);
      })
      .then(() => ProjectVersionsModel.getSmartRocketFormatContent(version.masterVersionId || version._id))
      .then((content) => utility.writeFile(JSON.stringify(content), path.join(localRepoPath, VERSIONFIELNAME)))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 更新/编辑文件信息
 * @param {String} fileId 文件id
 * @param {String} target 移动文件目标路径
 * @param {String} content 文件内容
 * @param {File} file 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function updateFile(
  fileId,
  { target = undefined, content = undefined },
  file = null,
  version = null,
  project = null,
  creator = null // eslint-disable-line comma-dangle
) {
  return new Promise((resolve, reject) => {
    let localRepoPath;
    let filePath;
    common.complementByFileId(fileId, file, version, project, creator)
      .then((res) => {
        file = res.file;
        version = res.version;
        project = res.project;
        creator = res.creator;
        localRepoPath = gitClient.getLocalRepoPath(creator, project, version);
        if (target) {
          filePath = path.join(localRepoPath, target);
          return fs.move(path.join(localRepoPath, file.path), filePath);
        }
        filePath = path.join(localRepoPath, file.path);
        return Promise.resolve();
      })
      .then(() => gitClient.checkoutBranch(creator, project, version))
      .then(() => {
        if (content || content === '') {
          return utility.writeFile(content, filePath);
        }
        return Promise.resolve();
      })
      .then(() => gitClient.addAllFilesInDirectory(filePath, creator, project, version))
      .then(() => updateVersionFiles(creator, project, version))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 更新文件/文件夹
 * @param {String} fileId 当前目录文件id
 * @param {String} filePath 文件/文件夹路径
 * @param {File} file 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @param {String} filename 文件名
 * @return {Promise}
 */
function updateFiles(
  fileId,
  filePath,
  file = undefined,
  version = undefined,
  project = undefined,
  creator = undefined,
  filename = undefined // eslint-disable-line comma-dangle
) {
  let dirPath = file.path;
  return new Promise((resolve, reject) => {
    let localRepoPath;
    common.complementByFileId(fileId, file, version, project, creator)
      .then((res) => {
        file = res.file;
        version = res.version;
        project = res.project;
        creator = res.creator;
        localRepoPath = gitClient.getLocalRepoPath(creator, project, version);
        return gitClient.checkoutBranch(creator, project, version);
      })
      .then(() => {
        if (filename) {
          return fs.remove(path.join(localRepoPath, file.path));
        }
        return Promise.resolve();
      })
      .then(() => {
        if (filename) {
          const newFilePath = path.join(localRepoPath, path.dirname(file.path), filename);
          // 此代码用于在替换文件夹时，为addAllFilesInDirectory方法传入新的目录, 以便给新的目录下新增.gitignore文件
          dirPath = path.join(path.dirname(file.path), filename);
          return fs.move(filePath, newFilePath, { overwrite: true });
        }
        if (file.kind === fileKind.file) {
          return fs.move(filePath, path.join(localRepoPath, file.path), { overwrite: true });
        }
        return moveDirectory2Repo(filePath, file, version, project, creator);
      })
      .then(() => gitClient.addAllFilesInDirectory(dirPath, creator, project, version))
      .then(() => updateVersionFiles(creator, project, version, true))
      .then(() => fs.remove(filePath))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 使用上传文件更新
 * @param {String} fileId 当前目录文件id
 * @param {String} uploadPath 文件路径
 * @param {File} file 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @param {String} filename 文件名
 * @param {String} zipFileNameEncoding 压缩文件解压文件名编码格式
 * @param {String} prefix 创建目录
 * @return {Promise}
 */
function updateByUploadFile(
  fileId,
  uploadPath,
  file = null,
  version = null,
  project = null,
  creator = null,
  filename = null, // eslint-disable-line comma-dangle
  zipFileNameEncoding,
  prefix = '',
) {
  return new Promise((resolve, reject) => {
    const destPath = `${uploadPath}.dest`;

    const doc = {
      repository: {
        type: repositoryType.file,
        url: uploadPath,
        branch: null,
        username: null,
      },
      extra: null,
    };
    const notCompiledKind = fileStatusKind.notCompiled;
    let creatorId;
    let projectId;
    let versionId;
    common.complementByFileId(fileId, file, version, project, creator)
      .then((res) => {
        file = res.file;
        creatorId = file.creatorId;
        projectId = file.projectId;
        versionId = file.versionId;
        version = res.version;
        project = res.project;
        creator = res.creator;

        if (file.kind === fileKind.directory) {
          /*
           * Zip upload and file replace both goes to this logic
           * If zip upload, we update the status to extracing
           * If file replace, we can't do update to pollute the current status
           */
          doc.status = fileStatusKind.extracting;
        }
        return File.updateOne({ _id: fileId }, doc);
      })
      .then(() => {
        doc.creatorId = creatorId;
        doc.projectId = projectId;
        doc.versionId = versionId;
        doc.fileId = fileId;
        if (file.directoryId) {
          publisher.publishFileMessage('update', doc);
        } else {
          publisher.publishRootDirectoryMessage('update', doc);
        }

        return fs.remove(destPath);
      })
      .then(() => {
        const pth = path.join(destPath, prefix);
        if (file.kind === fileKind.file) {
          return fs.move(uploadPath, pth);
        }
        if (fs.statSync(uploadPath).isDirectory()) {
          // 是目录就直接move
          return fs.move(uploadPath, pth);
        }
        return utility.uncompress(uploadPath, pth, zipFileNameEncoding);
      })
      .then(() => {
        // File replace do not need to do the status initialize
        if (file.kind === fileKind.file) {
          return Promise.resolve();
        }
        return updateFileStatus(fileId, notCompiledKind);
      },
      (err) => {
        logger.error('Failed to handle uploaded file', err);
        // 更新当前文件解压缩失败状态
        const doc = {
          status: fileStatusKind.extractFailed,
        };
        if (file.kind === fileKind.file) {
          doc.extra = fileUpdateErrorKind.uploadFile.moveFileFailed;
        } else {
          doc.extra = fileUpdateErrorKind.uploadFile.extractFailed;
        }
        return new Promise((resolve, reject) => {
          File.updateOne({ _id: fileId }, doc)
            .then(() => fs.remove(uploadPath))
            .then(() => {
              doc.creatorId = creatorId;
              doc.projectId = projectId;
              doc.versionId = versionId;
              doc.fileId = fileId;
              if (file.directoryId) {
                publisher.publishFileMessage('update', doc);
              } else {
                publisher.publishRootDirectoryMessage('update', doc);
              }
              reject(doc);
            })
            .catch(reject);
        });
      })
      .then(() => fs.remove(uploadPath))
      .then(() => updateFiles(fileId, destPath, file, version, project, creator, filename))
      .then(resolve)
      .catch((err) => {
        if (err.extra) {
          resolve();
        } else {
          reject(err);
        }
      });
  });
}

/**
 * 通过远程文件更新
 * @param {String} fileId 当前目录文件id
 * @param {String} url 远程文件路径
 * @param {File} file 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @param {string} zipFileNameEncoding 压缩文件解压文件名编码格式
 * @return {Promise}
 */
function updateByRemoteFile(
  fileId,
  url,
  file = null,
  version = null,
  project = null,
  creator = null, // eslint-disable-line comma-dangle
  zipFileNameEncoding
) {
  return new Promise((resolve, reject) => {
    const filename = `${fileId}-${randomstring.generate(16)}`;
    const filePath = path.join(cacheUtil.getConfigPath('uploadDir'), filename);

    const doc = {
      status: fileStatusKind.downloading,
      repository: {
        type: repositoryType.file,
        url,
        branch: null,
        username: null,
      },
    };
    let creatorId;
    let projectId;
    let versionId;
    const notCompiledKind = fileStatusKind.notCompiled;
    File.updateOne({ _id: fileId }, doc)
      .then(() => common.complementByFileId(fileId, file, version, project, creator))
      .then((res) => {
        file = res.file;
        creatorId = file.creatorId;
        projectId = file.projectId;
        versionId = file.versionId;
        version = res.version;
        project = res.project;
        creator = res.creator;
        doc.creatorId = creatorId;
        doc.projectId = projectId;
        doc.versionId = versionId;
        doc.fileId = fileId;
        if (file.directoryId) {
          publisher.publishFileMessage('update', doc);
        } else {
          publisher.publishRootDirectoryMessage('update', doc);
        }

        return download(url, cacheUtil.getConfigPath('uploadDir'), { filename });
      })
      .then(() => updateFileStatus(fileId, notCompiledKind),
        (err) => {
          logger.error('Failed to download remote file', err);
          // 更新当前文件下载失败状态
          const doc = {
            status: fileStatusKind.downloadFailed,
            extra: fileUpdateErrorKind.remoteFile.downloadFailed,
          };
          return new Promise((resolve, reject) => {
            File.updateOne({ _id: fileId }, doc)
              .then(() => fs.remove(filePath))
              .then(() => {
                doc.creatorId = creatorId;
                doc.projectId = projectId;
                doc.versionId = versionId;
                doc.fileId = fileId;
                if (file.directoryId) {
                  publisher.publishFileMessage('update', doc);
                } else {
                  publisher.publishRootDirectoryMessage('update', doc);
                }
                reject(doc);
              })
              .catch(reject);
          });
        })
      .then(() => updateByUploadFile(fileId, filePath, file, version, project, creator, null, zipFileNameEncoding))
      .then(resolve)
      .catch((err) => {
        if (err.extra) {
          resolve();
        } else {
          reject(err);
        }
      });
  });
}

/**
 * 通过git更新
 * @param {String} fileId 当前目录文件id
 * @param {String} url git远程仓库路径
 * @param {String} username 用户名
 * @param {String} password 密码
 * @param {String} branch 分支
 * @param {File} file 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @param {Number} remoteType 远程资源类型
 * @return {Promise}
 */
function updateByRemoteSource(
  fileId,
  url,
  { username = null, password = null, branch = 'master' },
  file = null,
  version = null,
  project = null,
  creator = null, // eslint-disable-line comma-dangle
  remoteType = repositoryType.git,
) {
  return new Promise((resolve, reject) => {
    const checkoutPath = path.join(cacheUtil.getConfigPath('uploadDir'), `${fileId}-${randomstring.generate(16)}`);
    let checkoutURL;
    switch (remoteType) {
      case repositoryType.svn:
        checkoutURL = url;
        break;
      default:
        // eslint-disable-next-line no-case-declarations
        const checkoutInfo = gitUrlParse(url);
        if (password) {
          checkoutInfo.user = `${encodeURIComponent(username)}:${encodeURIComponent(password)}`;
        }
        checkoutURL = gitUrlParse.stringify(checkoutInfo);
        break;
    }
    const doc = {
      status: fileStatusKind.cloning,
      repository: {
        type: remoteType,
        url: checkoutURL,
        branch,
        username,
      },
      extra: null,
    };
    let creatorId;
    let projectId;
    let versionId;
    const notCompiledKind = fileStatusKind.notCompiled;
    File.updateOne({ _id: fileId }, doc)
      .then(() => common.complementByFileId(fileId, file, version, project, creator))
      .then((res) => {
        file = res.file;
        creatorId = file.creatorId;
        projectId = file.projectId;
        versionId = file.versionId;
        version = res.version;
        project = res.project;
        creator = res.creator;
        doc.creatorId = creatorId;
        doc.projectId = projectId;
        doc.versionId = versionId;
        doc.fileId = fileId;
        if (file.directoryId) {
          publisher.publishFileMessage('update', doc);
        } else {
          publisher.publishRootDirectoryMessage('update', doc);
        }
        switch (remoteType) {
          case repositoryType.svn:
            return new Promise((resolve, reject) => {
              const params = ['--no-auth-cache'];
              if (username) {
                params.push(`--username=${username}`);
              }
              if (password) {
                params.push(`--password=${password}`);
              }
              if (checkoutURL.toLowerCase().startsWith('https')) {
                params.push('--trust-server-cert-failures="other,unknown-ca,cn-mismatch,expired"');
              }
              svnUltimate.commands.export(checkoutURL, checkoutPath, { params }, (err) => {
                if (err) return reject(err);
                return resolve();
              });
            });
          default:
            return SimpleGit().silent(true).clone(checkoutURL, checkoutPath, ['-b', branch, '--depth=1']);
        }
      })
      .then(() => updateFileStatus(fileId, notCompiledKind),
        (err) => {
          logger.error({}, err);
          let extra;
          switch (remoteType) {
            case repositoryType.svn:
              if (err.message.indexOf('E170000') !== -1 || err.message.indexOf('Unknown hostname') !== -1) {
                extra = fileUpdateErrorKind.svn.repositoryNotFound;
              } else if (err.message.indexOf('E170001') !== -1 || err.message.indexOf('Authentication error') !== -1) {
                extra = fileUpdateErrorKind.svn.authenticationFailed;
              } else {
                extra = fileUpdateErrorKind.svn.otherError;
              }
              break;
            default:
              if (err.message.indexOf('Repository Not Found') !== -1) {
                extra = fileUpdateErrorKind.git.repositoryNotFound;
              } else if (err.message.indexOf('Could not find remote branch') !== -1) {
                extra = fileUpdateErrorKind.git.branchNotFound;
              } else if (err.message.indexOf('Authentication failed') !== -1 || err.message.indexOf('could not read Username') !== -1) {
                extra = fileUpdateErrorKind.git.authenticationFailed;
              } else {
                extra = fileUpdateErrorKind.git.otherError;
              }
              break;
          }
          // 更新当前文件git检出失败状态
          const doc = {
            status: fileStatusKind.cloneFailed,
            extra,
          };
          return new Promise((resolve, reject) => {
            File.updateOne({ _id: fileId }, doc)
              .then(() => fs.remove(checkoutPath))
              .then(() => {
                doc.creatorId = creatorId;
                doc.projectId = projectId;
                doc.versionId = versionId;
                doc.fileId = fileId;
                if (file.directoryId) {
                  publisher.publishFileMessage('update', doc);
                } else {
                  publisher.publishRootDirectoryMessage('update', doc);
                }
                reject(doc);
              })
              .catch(reject);
          });
        })
      .then(() => updateFiles(fileId, checkoutPath, file, version, project, creator))
      .then(resolve)
      .catch((err) => {
        if (err.extra) {
          resolve();
        } else {
          reject(err);
        }
      });
  });
}

/**
 * 删除文件/文件夹
 * @param {String} fileId 当前目录文件id
 * @param {File} file 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function removeFile(fileId, file = null, version = null, project = null, creator = null) {
  return new Promise((resolve, reject) => {
    let localRepoPath;

    common.complementByFileId(fileId, file, version, project, creator)
      .then((res) => {
        file = res.file;
        version = res.version;
        project = res.project;
        creator = res.creator;
        localRepoPath = gitClient.getLocalRepoPath(creator, project, version);
        return gitClient.checkoutBranch(creator, project, version);
      })
      .then(() => {
        const filePath = path.join(localRepoPath, file.path);
        if (path.normalize(localRepoPath) === filePath) {
          return gitClient.emptyRepoDirectory(creator, project, '.', VERSIONFIELNAME, version);
        }
        return fs.remove(filePath);
      })
      .then(() => gitClient.addAllFilesInDirectory(file.path, creator, project, version))
      .then(() => updateVersionFiles(creator, project, version))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 删除某个项目版本下文件的执行结果
 * @param {String} versionId 项目版本id
 * @return {Promise}
 */
function removeResultOfExecutionInVersion(versionId) {
  versionId = mongoose.Types.ObjectId(versionId);
  const doc = {
    failedCount: {},
    succeedCount: {},
    statementCovered: {},
    branchCovered: {},
    conditionCovered: {},
    decisionCovered: {},
    mcdcCovered: {},
    invokeCovered: {},
    invokedFuncCovered: {},
  };
  const promises = [
    File.updateMany({ versionId }, { $set: doc }),
    functions.removeResultOfExecutionInVersion(versionId),
  ];
  return Promise.all(promises);
}

/**
 * 将新老数据合并成最新的数据(全局变量信息, 字面量信息, 类型系统)
 * @param {Object} oldObj 老数据
 * @param {Object} newObj 新数据
 * @return {Object}
 */
function mergeObject(oldObj, newObj) {
  if (!oldObj) {
    return newObj;
  }
  Object.keys(oldObj).forEach((key) => {
    if (!oldObj[key].specid) {
      delete oldObj[key];
    }
  });
  Object.keys(newObj).forEach((key) => {
    oldObj[key] = newObj[key];
  });
  return oldObj;
}

/**
 * 若全局变量信息已存在，更新之；若不存在，添加之
 * @param {String} projectId 项目id
 * @param {String} projectVersionId 项目版本id
 * @param {String} fileId 文件id
 * @param {Object} globals 全局变量信息
 * @return {Promise}
 */
function replaceGlobalVariables(projectId, projectVersionId, fileId, globals = {}) {
  return new Promise((resolve, reject) => {
    projectId = mongoose.Types.ObjectId(projectId);
    projectVersionId = mongoose.Types.ObjectId(projectVersionId);
    fileId = mongoose.Types.ObjectId(fileId);
    const doc = {
      projectId,
      versionId: projectVersionId,
      fileId,
      globals,
    };
    const fileGlobalVariables = new FileGlobalVariable(doc);
    let exist = false;
    fileGlobalVariables.save()
      .then((res) => Promise.resolve(res), (err) => {
        if (err.code === 11000) {
          exist = true;
          return Promise.resolve();
        }
        return Promise.reject(err);
      })
      .then(() => {
        if (exist) {
          // TODO maybe have concurrency issue, use __v?
          return Promise.resolve()
            .then(() => FileGlobalVariable.findOne({ fileId }))
            .then((res) => FileGlobalVariable.updateOne(
              { fileId },
              { $set: { globals: mergeObject(res.globals, globals) } }
            ));
        }
        return Promise.resolve();
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 若字面量信息已存在，更新之；若不存在，添加之
 * @param {String} projectId 项目id
 * @param {String} projectVersionId 项目版本id
 * @param {String} fileId 文件id
 * @param {Object} literals 字面量信息
 * @return {Promise}
 */
function replaceLiterals(projectId, projectVersionId, fileId, literals = {}) {
  return new Promise((resolve, reject) => {
    projectId = mongoose.Types.ObjectId(projectId);
    projectVersionId = mongoose.Types.ObjectId(projectVersionId);
    fileId = mongoose.Types.ObjectId(fileId);
    const doc = {
      projectId,
      versionId: projectVersionId,
      fileId,
      literals,
    };
    const fileLiterals = new FileLiteral(doc);
    let exist = false;
    fileLiterals.save()
      .then((res) => Promise.resolve(res), (err) => {
        if (err.code === 11000) {
          exist = true;
          return Promise.resolve();
        }
        return Promise.reject(err);
      })
      .then(() => {
        if (exist) {
          // TODO maybe have concurrency issue, use __v?
          return Promise.resolve()
            .then(() => FileLiteral.findOne({ fileId }))
            .then((res) => FileLiteral.updateOne(
              { fileId },
              { $set: { literals: mergeObject(res.literals, literals) } },
              { checkKeys: false }
            ));
        }
        return Promise.resolve();
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 若类型系统已存在，更新之；若不存在，添加之
 * @param {String} projectId 项目id
 * @param {String} projectVersionId 项目版本id
 * @param {String} fileId 文件id
 * @param {Object} types 类型系统信息
 * @return {Promise}
 */
async function replaceTypeSystem(projectId, projectVersionId, fileId, types = {}) {
  projectId = mongoose.Types.ObjectId(projectId);
  fileId = mongoose.Types.ObjectId(fileId);
  const versionId = mongoose.Types.ObjectId(projectVersionId);
  const fileTypes = await FileTypeSystem.find({ fileId }).lean();
  const inserts = [];
  const removes = [];
  // 类型存储key的拆分个数
  const len = 10000;
  fileTypes.forEach(({ _id, types }) => {
    Object.keys(types).forEach((key) => {
      // 保留特化类型老数据
      if (types[key] && types[key].specid) {
        inserts.push({
          projectId,
          versionId,
          fileId,
          types: {
            [key]: types[key],
          },
        });
      }
      removes.push(_id);
    });
  });
  const typeKeys = utility.arrayChunk(Object.keys(types), len);
  typeKeys.forEach((keys) => {
    const obj = {
      projectId,
      versionId,
      fileId,
      types: {},
    };
    keys.forEach((key) => {
      obj.types[key] = types[key];
    });
    inserts.push(obj);
  });
  await FileTypeSystem.deleteMany({ _id: { $in: removes } });
  await FileTypeSystem.insertMany(inserts, { checkKeys: false });
}

/**
 * 若诊断消息已存在，更新之；若不存在，添加之
 * @param {String} projectId 项目id
 * @param {String} projectVersionId 项目版本id
 * @param {String} fileId 文件id
 * @param {Array} diagMessages 诊断消息列表
 * @return {Promise}
 */
function replaceDiag(projectId, projectVersionId, fileId, diagMessages = [], includeFiles = {}, currentFile) {
  return new Promise((resolve, reject) => {
    projectId = mongoose.Types.ObjectId(projectId);
    projectVersionId = mongoose.Types.ObjectId(projectVersionId);
    fileId = mongoose.Types.ObjectId(fileId);
    let caseSensitive;
    let exist = false;
    let compileFailed = false;
    let doc = {};
    let projectVersion;

    const filePathSet = new Set();
    diagMessages.forEach(({ file, type }) => {
      if (type === 'error' || type === 'fatal error') {
        compileFailed = true;
      }
      if (file) {
        filePathSet.add(getNormalizePath(file));
      }
    });
    Object.values(includeFiles).forEach(({ file }) => {
      if (file) {
        filePathSet.add(getNormalizePath(file));
      }
    });

    const projectVersionMap = new Map();
    const environmentMap = new Map();
    ProjectVersion.findById(projectVersionId)
      .then((version) => {
        projectVersion = version;
        caseSensitive = version.caseSensitive;
        projectVersionMap.set(version.projectId.toString(), version._id.toString());
        const promise = [];
        const versionIdArr = [projectVersionId];
        if (version.environmentId && version.environmentId.length > 0) {
          versionIdArr.push(...version.environmentId);
          promise.push(ProjectVersion.find({ _id: { $in: version.environmentId } }, ['_id', 'currentCommitId']));
        } else {
          promise.push(Promise.resolve());
        }
        promise.push(File.find(
          {
            versionId: { $in: versionIdArr },
            path: {
              $in: Array.from(filePathSet).map((filePath) => {
                let reg = filePath;
                if (!caseSensitive) {
                  reg = new RegExp(`^${utility.regExpQuote(getNormalizePath(filePath))}$`, 'i'); // 不区分大小写
                }
                return reg;
              }),
            },
            kind: fileKind.file,
          }
        ));
        return Promise.all(promise);
      })
      .then(([compileVersions, resFiles]) => {
        if (compileVersions) {
          compileVersions.forEach((compileVersion) => {
            environmentMap.set(compileVersion.currentCommitId, compileVersion._id.toString());
          });
        }
        const fileMap = new Map();
        resFiles.forEach((file) => {
          fileMap.set(`${file.versionId.toString()}|${caseSensitive ? file.path : file.path.toLowerCase()}`, file);
        });
        // 补全文件id
        diagMessages.forEach((diag) => {
          let f = diag.file;
          if (diag.file && diag.url) {
            const file1 = includeFiles[diag.file];
            if (file1) {
              diag.file = path.isAbsolute(file1.file) ? file1.file : getNormalizePath(file1.file);
            }
            let versionId = projectVersionMap.get(path.basename(diag.url));
            if (!versionId && diag.commit) {
              versionId = environmentMap.get(diag.commit);
            }
            const file = fileMap.get(`${versionId}|${getNormalizePath(caseSensitive ? diag.file : diag.file.toLowerCase())}`);
            if (file) {
              diag.fileId = file._id.toString(); // eslint-disable-line no-underscore-dangle
              diag.versionId = versionId;
            }
          }
          const parentArr = [];

          while (f) {
            const item = includeFiles[f];
            if (item && item.froms && item.froms.length) {
              const from = item.froms[0];
              const { line, column } = from.location.begin;
              if (getNormalizePath(from.file) === getNormalizePath(currentFile)) {
                // 入口文件
                parentArr.push({
                  line,
                  column,
                  file: path.isAbsolute(from.file) ? from.file : getNormalizePath(from.file),
                  fileId,
                  versionId: projectVersionId,
                });
                break;
              }
              const parent = includeFiles[from.file];
              if (!parent) {
                break;
              }
              if (parent.url) {
                let versionId = projectVersionMap.get(path.basename(parent.url));
                if (!versionId && parent.commit) {
                  versionId = environmentMap.get(parent.commit);
                }
                const file = fileMap.get(`${versionId}|${getNormalizePath(caseSensitive ? parent.file : parent.file.toLowerCase())}`);
                if (file) {
                  parent.fileId = file._id; // eslint-disable-line no-underscore-dangle
                  parent.versionId = file.versionId;
                }
              }
              parentArr.push({
                line,
                column,
                file: path.isAbsolute(parent.file) ? parent.file : getNormalizePath(parent.file),
                fileId: parent.fileId,
                versionId: parent.versionId,
              });
              f = from.file;
            } else {
              break;
            }
          }
          diag.includePath = parentArr;
        });
        function resc(diagMsgs, msgs) {
          if (!utility.isArrayType(diagMsgs)) {
            return;
          }
          diagMsgs.forEach(({
            file,
            line,
            column,
            message,
            children,
          }) => {
            if (file && message) {
              msgs.push(`${file}[line:${line}, column:${column}]${message}`);
            }
            resc(children, msgs);
          });
        }
        let desc = [];
        if (compileFailed) {
          resc(diagMessages, desc);
        }
        desc = desc.join(';');
        operationLogs.recordOperationLog({
          userId: projectVersion.creatorId,
          projectId,
          versionId: projectVersion._id,
          operationType: 'analyze',
          operationObject: 'file',
          name: currentFile,
          fileId,
          operationDesc: desc,
          operationResult: compileFailed ? 2 : 1,
        });
        // 保存
        doc = {
          projectId,
          versionId: projectVersionId,
          fileId,
          diagMessages,
        };
        const fileDiag = new FileDiag(doc);
        return fileDiag.save();
      })
      .then((res) => Promise.resolve(res), (err) => {
        if (err.code === 11000) {
          exist = true;
          return FileDiag.deleteOne({ fileId });
        }
        return Promise.reject(err);
      })
      .then((res) => {
        if (exist) {
          const fileDiag = new FileDiag(doc);
          return fileDiag.save();
        }
        return Promise.resolve(res);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 若宏替换信息已存在，更新之；若不存在，添加之
 * @param {String} projectId 项目id
 * @param {String} projectVersionId 项目版本id
 * @param {String} fileId 文件id
 * @param {Array} macros 宏替换列表
 * @return {Promise}
 */
async function replaceMacro(projectId, projectVersionId, fileId, macros = []) {
  const existed = await FileMacro.findOne({ fileId: mongoose.Types.ObjectId(fileId) }, { fileId: 1 });
  const doc = {
    projectId: mongoose.Types.ObjectId(projectId),
    versionId: mongoose.Types.ObjectId(projectVersionId),
    fileId: mongoose.Types.ObjectId(fileId),
    macros,
  };
  if (existed) {
    await FileMacro.collection.updateOne({
      fileId: mongoose.Types.ObjectId(fileId),
    }, { $set: doc });
  } else {
    await FileMacro.collection.insertOne(doc);
  }
}

/**
 * 获取类型系统json信息
 * @param {String} fileId 文件Id
 * @param {Object} typeSystem 类型系统
 * @returns {Object}
 */
function getTypesJson(fileId, typeSystem) {
  const types = typeSystem ? typeSystem.types : {};
  return { fileId, types: types || {} };
}

/**
 * 判断func是否为冻结状态
 * @param {Object} func 函数
 * @return {Promise<unknown>}
 */
function getCompilingFunction(func) {
  return new Promise((resolve, reject) => {
    getFile(func.fileId)
      .then((f) => {
        if (f.status === fileStatusKind.compiled || func.sourceParsedFile.length > 0) {
          resolve(func);
        }
        resolve();
      })
      .catch(reject);
  });
}

/**
 * 插入特化的字面量信息
 * @param {String} fileId 文件id
 * @param {String} elementId 文件id
 * @param {Object} literals 字面量信息
 * @return {Promise}
 */
function mergeSpecLiterals(fileId, elementId, literals = {}) {
  if (utility.isEmpty(literals)) {
    return Promise.resolve();
  }
  const data = {};
  Object.keys(literals).forEach((literalName) => {
    const literal = literals[literalName];
    Object.keys(literal).forEach((key) => {
      data[`literals.${literalName}.${key}`] = literal[key];
    });
    data[`literals.${literalName}.specid.${elementId}`] = 1;
  });
  return Promise.resolve()
    .then(() => FileLiteral.updateOne(
      { fileId: mongoose.Types.ObjectId(fileId) },
      { $set: data, $inc: { __v: 1 } }
    ));
}

/**
 * 插入特化的的全局变量信息
 * @param {String} fileId 文件id
 * @param {String} elementId 文件id
 * @param {Object} globalVariables 全局变量信息
 * @return {Promise}
 */
function mergeSpecGlobalVariables(fileId, elementId, globalVariables = {}) {
  if (utility.isEmpty(globalVariables)) {
    return Promise.resolve();
  }
  const data = {};
  Object.keys(globalVariables).forEach((globalVariableName) => {
    const globalVariable = globalVariables[globalVariableName];
    Object.keys(globalVariable).forEach((key) => {
      data[`globals.${globalVariableName}.${key}`] = globalVariable[key];
    });
    data[`globals.${globalVariableName}.specid.${elementId}`] = 1;
  });
  return Promise.resolve()
    .then(() => FileGlobalVariable.updateOne(
      { fileId: mongoose.Types.ObjectId(fileId) },
      { $set: data, $inc: { __v: 1 } }
    ));
}

/**
 * 插入特化的的类型系统信息
 * @param {String} projectId 项目id
 * @param {String} versionId 版本id
 * @param {String} fileId 文件id
 * @param {String} elementId 文件id
 * @param {Object} typeSystem 类型系统信息
 * @return {Promise}
 */
function mergeSpecTypeSystem(fileId, projectId, versionId, elementId, typeSystem = {}) {
  if (utility.isEmpty(typeSystem)) {
    return Promise.resolve();
  }
  const inserts = [];
  Object.keys(typeSystem).forEach((typeName) => {
    const data = {
      projectId,
      versionId,
      fileId,
      types: {
        [typeName]: {
          ...typeSystem[typeName],
          specid: {
            [elementId]: 1,
          },
        },
      },
    };
    inserts.push(data);
  });
  return FileTypeSystem.insertMany(inserts, { checkKeys: false });
}

/**
 * 删除特化所产生的类型系统信息
 * @param {Object} obj 信息
 * @param {Array} elementIds 元素id
 * @return {Object}
 */
function filterUnusedData(obj, elementIds) {
  Object.keys(obj).forEach((key) => {
    if (!obj[key].specid) {
      return;
    }
    elementIds.forEach((elementId) => {
      elementId = elementId.toString();
      if (obj[key].specid[elementId]) {
        delete obj[key].specid[elementId];
      }
    });
    if (utility.isEmpty(obj[key].specid)) {
      delete obj[key];
    }
  });
  return obj;
}

/**
 * 删除特化所产生的类型系统信息
 * @param {String} fileId 文件id
 * @param {Array} elementIds 元素id
 * @return {Promise}
 */
async function removeSpecTypeSystem(fileId, elementIds) {
  if (elementIds.length === 0) {
    return Promise.resolve();
  }
  const fileTypes = await FileTypeSystem.find({ fileId }, ['_id', 'types']).lean();
  if (!fileTypes || !fileTypes.length === 0) {
    return Promise.resolve();
  }
  return Promise.all(fileTypes.map(({ _id, types }) => {
    if (!types || utility.isEmpty(types)) {
      return FileTypeSystem.deleteOne({ _id });
    }
    const newTypes = filterUnusedData(types, elementIds);
    if (utility.isEmpty(newTypes)) {
      return FileTypeSystem.deleteOne({ _id });
    }
    // 没有特化的类型就直接返回
    if (Object.keys(types).length === Object.keys(newTypes).length) {
      return Promise.resolve();
    }
    return FileTypeSystem.updateOne(
      {
        _id,
      },
      {
        $set: {
          types: newTypes,
        },
      },
      { checkKeys: false }
    );
  }));
}

/**
 * 删除特化所产生的全局变量信息
 * @param {String} fileId 文件id
 * @param {Array} elementIds 元素id
 * @return {Promise}
 */
function removeSpecGlobalVariables(fileId, elementIds) {
  if (elementIds.length === 0) {
    return Promise.resolve();
  }
  return Promise.resolve()
    .then(() => FileGlobalVariable.findOne({ fileId: mongoose.Types.ObjectId(fileId) }).lean())
    .then((fileGlobalVariable) => {
      if (!fileGlobalVariable || !fileGlobalVariable.globals || utility.isEmpty(fileGlobalVariable.globals)) {
        return Promise.resolve();
      }
      fileGlobalVariable.globals = filterUnusedData(fileGlobalVariable.globals, elementIds);
      return FileGlobalVariable.updateOne(
        {
          fileId: mongoose.Types.ObjectId(fileId),
          // eslint-disable-next-line no-underscore-dangle
          __v: fileGlobalVariable.__v,
        },
        {
          globals: fileGlobalVariable.globals,
          $inc: { __v: 1 },
        },
      )
        .then((res) => {
          if (res.nModified === 0) {
            // TODO concurrency issue
          }
          return Promise.resolve();
        });
    });
}

/**
 * 删除特化所产生的字面量信息
 * @param {String} fileId 文件id
 * @param {Array} elementIds 元素id
 * @return {Promise}
 */
function removeSpecLiterals(fileId, elementIds) {
  if (elementIds.length === 0) {
    return Promise.resolve();
  }
  return Promise.resolve()
    .then(() => FileLiteral.findOne({ fileId: mongoose.Types.ObjectId(fileId) }).lean())
    .then((fileLiteral) => {
      if (!fileLiteral || !fileLiteral.literals || utility.isEmpty(fileLiteral.literals)) {
        return Promise.resolve();
      }
      fileLiteral.literals = filterUnusedData(fileLiteral.literals, elementIds);
      return FileLiteral.updateOne(
        {
          fileId: mongoose.Types.ObjectId(fileId),
          // eslint-disable-next-line no-underscore-dangle
          __v: fileLiteral.__v,
        },
        {
          literals: fileLiteral.literals,
          $inc: { __v: 1 },
        },
      )
        .then((res) => {
          if (res.nModified === 0) {
            // TODO concurrency issue
          }
          return Promise.resolve();
        });
    });
}

/**
 * 计算并保存文件行数
 * @param {String} fileId 文件id
 * @param {String} filePath 文件路径
 * @returns {Promise<Number>}
 */
async function computeNumberOfLines(fileId, filePath) {
  // eslint-disable-next-line no-use-before-define
  const {
    total, source, empty, comment,
  } = await computeFileCodeLines(filePath);
  await File.updateOne({ _id: mongoose.Types.ObjectId(fileId) }, {
    $set: {
      numberOfLines: total,
      sourceLines: source,
      commentLines: comment,
      emptyLines: empty,
    },
  });
}

// 已废弃
function computeFileLines(fileLocation) {
  let linesCount = 0;
  let hasContent;
  return new Promise((resolve, reject) => {
    fs.createReadStream(fileLocation).on('data', (buf) => {
      if (buf.length && !hasContent) {
        hasContent = true;
      }
      const newLineCharacter = Buffer.from('\n', 'ascii');
      let idx = -1;
      let count = 0;
      // eslint-disable-next-line no-cond-assign
      while ((idx = buf.indexOf(newLineCharacter, idx + 1)) > -1) count += 1;
      linesCount += count;
    }).on('end', () => {
      resolve(hasContent ? linesCount + 1 : 0);
    }).on('error', reject);
  });
}

async function computeFileCodeLines(filePath, {
  debug = false,
} = {}) {
  let emptyLine = 0; // 空行
  let commentLine = 0; // 注释行
  let sourceLine = 0; // 有效代码行

  let lineNum = 0;
  let flagMultiStart = false; // 表示当前已进入多行注释范围
  let flagString = false; // 表示当前已进入字符串范围

  function scan(line) {
    lineNum += 1;

    line = line.trim();
    if (!line.length) {
      emptyLine += 1;
      return { emptyLine, commentLine, sourceLine };
    } if (!flagMultiStart
      && !flagString
      && line.startsWith('//')) {
      commentLine += 1;
      return { emptyLine, commentLine, sourceLine };
    }
    let isSourceLine = false;
    for (let index = 0; index < line.length; index += 1) {
      if (
        !flagString
        && !flagMultiStart
        && line[index - 1] === '/'
        && line[index] === '*'
      ) {
        // 多行注释开始符
        flagMultiStart = true;
      } else if (
        !flagString
        && flagMultiStart
        && line[index - 1] === '*'
        && line[index] === '/'
      ) {
        // 多行注释结束符
        flagMultiStart = false;
      } else {
        if (
          !flagMultiStart
          && line[index].trim().length // 排除空字符，例如\v\t\n空格
          && line[index] !== '/'
          && line[index] !== '*'
        ) {
          isSourceLine = true;
        }
        if (!flagMultiStart
          && line[index] === '"'
          && line[index - 1] !== '\\') {
          // 判断是否在字符串内部
          flagString = !flagString;
        }
      }
    }
    if (isSourceLine) {
      sourceLine += 1;
    } else {
      commentLine += 1;
    }

    return { emptyLine, commentLine, sourceLine };
  }

  const stream = fs.createReadStream(filePath);
  const rl = readline.createInterface(stream);
  let fileTotalLine = 1;
  return new Promise((res, rej) => {
    stream.on('error', rej);
    // 统计文件代码行数
    stream.on('data', (chunk) => {
      for (let i = 0; i < chunk.length; i += 1) {
        // 计算\n
        if (chunk[i] === 10) {
          fileTotalLine += 1;
        }
      }
    });
    rl.on('line', (line) => {
      scan(line);
    });

    rl.on('close', () => {
      // 文件最后一行是空行也算进去
      if (fileTotalLine - (emptyLine + commentLine + sourceLine) === 1) {
        emptyLine += 1;
      }
      res({
        empty: emptyLine,
        comment: commentLine,
        source: sourceLine,
        total: emptyLine + commentLine + sourceLine,
      });
    });
  });
}

function getLinesOfContent(filepath, {
  start = 1,
  end,
} = {}) {
  const rs = fs.createReadStream(filepath, {
    highWaterMark: 1024,
  });
  const rl = readline.createInterface({
    input: rs,
  });
  let numOfLine = 0;
  const lines = [];
  return new Promise((res, rej) => {
    rl.on('line', (line) => {
      numOfLine += 1;
      if (end && numOfLine > end) {
        rs.close();
        rl.close();
        return res(lines);
      }
      if (numOfLine >= start) {
        lines.push({
          line: numOfLine,
          content: line,
        });
      }
      return 1;
    });
    rl.on('close', () => {
      res(lines);
    });
    rs.on('error', rej);
  });
}

/**
 * 批量从一个文件内获取需要的行内容
 * @param {string} fileId 文件id
 * @param {Array} ranges range数组，数组内每个元素为一个二元数组，代表开始行和结束行
 * @returns {Promise<Array>}
 */
async function getLinesOfContentBatch(fileId, ranges) {
  const ret = [];
  const [maxLine] = ranges.map(([_, end]) => end).sort((a, b) => b - a);
  if (!maxLine) {
    return ret;
  }
  const fileStream = await getFileContentStream(fileId, null, true);

  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });
  let lineNum = 0;

  let lastChunk;
  fileStream.on('data', (d) => {
    lastChunk = d.toString();
  });

  const processLine = (line) => {
    lineNum += 1;
    for (let index = 0; index < ranges.length; index += 1) {
      const [start, end] = ranges[index];
      if (lineNum >= start && lineNum <= end) {
        ret[index] = ret[index] || [];
        ret[index].push(line);
      }
    }
  };
  // eslint-disable-next-line no-restricted-syntax
  for await (const line of rl) {
    processLine(line);
    if (lineNum >= maxLine) {
      fileStream.destroy();
      rl.close();
      break;
    }
  }
  if (maxLine > lineNum) {
    // 检查末尾行是否是空行？即结束字符是换行符
    if (lastChunk.endsWith('\n')) {
      processLine(''); // 补一个空行
    }
  }
  return ret;
}
/**
 * 读取gogos仓库文件内容
 * @param {String} fileId 文件id
 * @param {Number} start  开始读取行
 * @param {Number} end 读取结束行
 * @returns
 */
async function getLinesOfRemoteFileStream(fileId, {
  start = 1,
  end,
}) {
  const rs = await getFileContentStream(fileId, null, true);
  const rl = readline.createInterface({
    input: rs,
  });
  let numOfLine = 0;
  const lines = [];
  return new Promise((res, rej) => {
    rl.on('line', (line) => {
      numOfLine += 1;
      if (end && numOfLine > end) {
        rs.end();
        rl.close();
        return res(lines);
      }
      if (numOfLine >= start) {
        lines.push({
          line: numOfLine,
          content: line,
        });
      }
      return 1;
    });
    rl.on('close', () => {
      res(lines);
    });
    rs.on('error', rej);
  });
}
/**
 * 屏蔽文件
 * @param {String} fildId  文件id
 * @param {Number} status 屏蔽状态 1：取消屏蔽，2：屏蔽
 */
async function shieldFiles(fileId, status) {
  const incDoc = { $inc: {} };
  const file = await getFile(fileId);
  const {
    kind, fileCount, directoryId, shieldFilesCount, versionId, path,
  } = file;
  await ProjectVersionsModel.updateRegressionStatusPrompt(versionId,
    ProjectVersionsModel.regressionStatusPrompt.modifyOriginalVersion);
  if (kind === fileKind.file) {
    const extname = common.renameMongoKey(file.extname);
    if (status === shieldFilesStatus.disable) {
      await File.updateOne({ _id: mongoose.Types.ObjectId(fileId) },
        { $set: { shieldFilesCount: fileCount } });
      incDoc.$inc[`shieldFilesCount.${extname}`] = 1;
    } else if (status === shieldFilesStatus.enable) {
      shieldFilesCount[extname] = 0;
      await File.updateOne({ _id: mongoose.Types.ObjectId(fileId) }, { $set: { shieldFilesCount } });
      incDoc.$inc[`shieldFilesCount.${extname}`] = -1;
    }
    increaseDirectories(directoryId, incDoc);
  } else if (kind === fileKind.directory) {
    const files = await File.find({
      versionId: mongoose.Types.ObjectId(versionId),
      kind: fileKind.file,
      path: new RegExp(`^${utility.regExpQuote(`${path}/`)}`),
      extname: { $ne: '' },
    });
    files.forEach((childrenFile) => {
      const extname = common.renameMongoKey(childrenFile.extname);
      const shieldFilesCountValue = childrenFile.shieldFilesCount[extname];
      if (status === shieldFilesStatus.disable) {
        // 过滤未被屏蔽的文件
        if (!shieldFilesCountValue || shieldFilesCountValue === 0) {
          shieldFiles(childrenFile._id, status);
        }
      } else if (status === shieldFilesStatus.enable && shieldFilesCountValue === 1) { // 过滤被屏蔽的文件
        shieldFiles(childrenFile._id, status);
      }
    });
  }
}

/**
 * 组合文件函数关系
 * @param {array} fileIds  文件id
 * @param {array} functionIds 函数id
 * @returns
 */
function aggregateFileByfunctionIds(fileIds = undefined, functionIds) {
  return new Promise((resolve, reject) => {
    Func.aggregate([{
      $match: {
        _id: {
          $in: functionIds.map((functionId) => mongoose.Types.ObjectId(functionId)),
        },
      },
    },
    {
      $group: {
        _id: '$fileId',
        functionIds: { $addToSet: '$_id' },
      },
    }]).then((resFileIds) => {
      if (!fileIds) {
        fileIds = [];
      }
      resFileIds.forEach((file) => {
        if (fileIds.length === 0) {
          fileIds.push({
            fileId: file._id,
            functionIds: file.functionIds,
          });
        } else {
          for (let i = 0; i < fileIds.length; i += 1) {
            if (!fileIds[i].functionIds) {
              fileIds[i].functionIds = [];
            }
            if (fileIds[i].fileId === file._id.toString()) {
              fileIds[i].functionIds = file.functionIds;
            }
          }
        }
      });
      resolve(fileIds);
    }).catch(reject);
  });
}

async function readLocalFile(creatorId, projectId, fileRelativePath) {
  const repoPath = gitClient.getLocalRepoPath(creatorId, projectId);
  const filePath = path.join(repoPath, fileRelativePath);
  const buf = await fs.readFile(filePath);
  return buf.toString();
}
function getFileById(fileId) {
  return File.findById(fileId).lean();
}
/**
 * 获取文件
 * @param {string} versionId  版本Id
 * @param {Array} sourceFileExts 文件后缀名
 * @param {Object} fields 获取显示的字段
 * @param {String} mapKey 返回的文件map以什么做key
 * @returns {Map} fileMap
 */
async function getSourceFileByVersionId(versionId, sourceFileExts, fields = {}, mapKey = '_id') {
  const allFileMap = new Map();
  const filter = {
    versionId,
    $or: [
      { kind: fileKind.file },
      { kind: fileKind.fileOutsideProject },
    ],
    $and: [{ fileName: { $nin: utility.excludedFiles() } }],
  };
  if (sourceFileExts && sourceFileExts.length > 0) {
    filter.$or.push({
      exname: { $in: sourceFileExts },
    });
  }
  const allFiles = await File.find(filter, fields).lean();
  allFiles.forEach((file) => {
    allFileMap.set(file[mapKey].toString(), file);
  });

  return allFileMap;
}
/**
 * 更新文件编码格式
 * @param {String|ObjectId} fileId 文件id
 * @param {String} encoding 编码格式
 * @param {Boolean} updateRelationVersion 是否更新关联版本的文件编码格式
 * @returns
 */
async function updateEncoding(fileId, encoding = 'utf-8', updateRelationVersion = true) {
  if (updateRelationVersion) {
    const file = await File.findById(fileId);
    const versions = await ProjectVersionsModel.getRelationVersions(file.versionId, { includeSelf: true });
    return File.updateMany({ versionId: { $in: versions.map((item) => item._id) }, path: file.path }, { $set: { encoding } });
  }
  return File.updateOne({ _id: mongoose.Types.ObjectId(fileId) }, { $set: { encoding } });
}
async function updateEncodingByVersionId(versionId, encoding = 'utf-8') {
  await File.updateMany({ versionId: mongoose.Types.ObjectId(versionId), encoding: null, kind: fileKind.file }, { $set: { encoding } });
}

async function getContentGroupByFile(funcs, sourceParsedFile) {
  if (!funcs) {
    return [];
  }
  const fileMap = new Map();
  const existsFiles = new Map();
  const newFiles = new Map();
  const paths = [];
  const filesOutsideProject = await File.find(
    { versionId: sourceParsedFile.versionId, kind: fileKind.fileOutsideProject }, ['path']
  );
  filesOutsideProject.forEach((file) => {
    existsFiles.set(file.path, file._id.toString());
  });
  funcs.forEach((func) => {
    if (!fileMap.has(func.location.path)) {
      fileMap.set(func.location.path, {
        contentFuncs: [],
        fileId: '',
        insideProject: func.location.isInProjectFile,
      });
    }
    fileMap.get(func.location.path).contentFuncs.push(func);
    if (existsFiles.has(func.location.path)) {
      fileMap.get(func.location.path).fileId = existsFiles.get(func.location.path);
      // 项目外文件
    } else {
      paths.push(func.location.path);
      if (!func.location.isInProjectFile) {
        if (newFiles.has(func.location.path)) {
          return;
        }
        newFiles.set(func.location.path, {
          path: func.location.path,
          fileName: path.basename(func.location.path),
          kind: fileKind.fileOutsideProject,
          creatorId: sourceParsedFile.creatorId,
          projectId: sourceParsedFile.projectId,
          versionId: sourceParsedFile.versionId,
        });
      }
    }
  });
  if (newFiles.size > 0) {
    await File.collection.insertMany([...newFiles.values()], { ordered: false });
  }
  await utility.arrayChunkOperation(utility.arrayChunk(paths, 50), 1, async (paths) => {
    const filesInfo = await File.find({ versionId: sourceParsedFile.versionId, path: { $in: paths } }, ['path']);
    filesInfo.forEach((fileInfo) => {
      fileMap.get(fileInfo.path).fileId = fileInfo._id.toString();
    });
  });
  return [...fileMap.values()];
}

async function importEnvironmentFiles(uploadPath, user, project, environment, zipFileNameEncoding) {
  const file = environment.rootDirectory;
  const { _id: fileId, filename } = file;
  const destPath = `${uploadPath}.dest`;
  const doc = {
    creatorId: user._id,
    projectId: project._id,
    versionId: environment._id,
    fileId,
  };
  try {
    await utility.uncompress(uploadPath, destPath, zipFileNameEncoding);
  } catch (error) {
    logger.error('Failed to uncompress file', error);
    await ProjectVersion.deleteOne({ _id: environment._id });
    return { extra: fileUpdateErrorKind.uploadFile.extractFailed, message: error.toString() };
  }
  try {
    await utility.removeFile(uploadPath);
    await File.updateOne({ _id: fileId }, doc);
    const envConfigFilePath = path.join(destPath, 'environment_config.json');
    if (await utility.doesFileExist(envConfigFilePath)) {
      const envConfig = await utility.readFile(envConfigFilePath);
      await utility.removeFile(envConfigFilePath);
      await ProjectVersion.updateOne({ _id: environment._id }, JSON.parse(envConfig));
    }
    await updateFiles(fileId, destPath, file, environment, project, user, filename);
  } catch (error) {
    await ProjectVersion.deleteOne({ _id: environment._id });
    logger.error('Failed to update file', error);
    throw new Error(error);
  }
  return { extra: null, message: 'success' };
}
/**
 *  读取文件注释信息
 * @param {String|ObjectId} fileId 文件id
 * @param {String|ObjectId} versionId 版本id
 * @returns
 */
async function readFileComment(fileId, versionId) {
  const fileStream = await getFileContentStream(fileId, null, true);
  if (!fileStream) {
    return;
  }
  const r1 = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });
  const commentData = [];
  let lineNum = 0;
  const expkeys = Object.keys(fileCommentTypeExp);
  await new Promise((resolve, reject) => {
    r1.on('line', (lineData) => {
      lineNum += 1;
      if (lineData) {
        for (let i = 0; i < expkeys.length; i += 1) {
          const key = expkeys[i];
          if (lineData.match(fileCommentTypeExp[key])) {
            const matchData = lineData.match(fileCommentTypeExp[key]);
            const newCommentTypeKey = key.substring(0, key.length - 'RegularExp'.length);
            let commentInfo = matchData[matchData.length - 1] || '';
            if (commentInfo.indexOf('*/') !== -1) {
              commentInfo = commentInfo.replace('*/', '');
            }
            commentInfo = commentInfo.trim();
            commentData.push({
              versionId,
              fileId,
              comment: commentInfo,
              line: lineNum,
              type: fileCommentType[newCommentTypeKey],
            });
          }
        }
      }
    });
    r1.on('close', () => {
      fileStream.destroy();
      r1.close();
      resolve();
    });
  });
  await FileComment.deleteMany({ fileId });
  await FileComment.insertMany(commentData, { lean: true });
}
/**
 * 删除文件旧的类型系统，保留原先特化的类型
 * @param {String} projectId 项目id
 * @param {String} versionId 项目版本id
 * @param {String} fileId 文件id
 * @return {Promise}
 */
async function deleteFileOriginalTypeSystem(projectId, versionId, fileId) {
  const fileTypes = await FileTypeSystem.find({ fileId }).lean();
  const inserts = [];
  const removes = [];
  fileTypes.forEach(({ _id, types }) => {
    Object.keys(types).forEach((key) => {
      // 保留特化类型数据
      if (types[key] && types[key].specid) {
        inserts.push({
          projectId,
          versionId,
          fileId,
          types: {
            [key]: types[key],
          },
        });
      }
      removes.push(_id);
    });
  });
  await FileTypeSystem.deleteMany({ _id: { $in: removes } });
  await FileTypeSystem.insertMany(inserts, { checkKeys: false });
}

async function searchFiles(user, project, version, string, option = {}, limit = 100) {
  const result = {
    fileCount: 0,
    resultCount: 0,
    hasMoreResult: false,
    result: [],
  };
  if (!string) return result;
  string = utility.regExpQuote(string);
  const suffixes = getSearchSuffixes(version);
  if (!suffixes.length) {
    // no file to search
    throw new PhoenixError('no file type in this version');
  }
  option.suffixes = suffixes;
  // environment
  const versionIds = [];
  if (version.environmentId) {
    versionIds.push(...version.environmentId);
  }
  versionIds.push(version._id);
  const versionsToSearch = await ProjectVersion.find({ _id: { $in: versionIds } });
  const versionIdsString = versionIds.map((data) => data.toString());
  versionsToSearch.sort((a, b) => versionIdsString.indexOf(a._id.toString()) - versionIdsString.indexOf(b._id.toString()));
  const filesPath = [];
  /**
   * each result in one version:
   * {
   * fileCount,
   * resultCount,
   * hasMoreResult,
   * result: [
   *  {
   *  filePath,
   *  match:[{
   *    line, col, content
   *    }]
   *  }
   * ]
   * filesPath:[paths need to find id from db]
   * }
   */
  for (let i = 0; i < versionsToSearch.length; i += 1) {
    if (result.resultCount >= limit) {
      result.hasMoreResult = true;
      break;
    }
    const versionInfo = versionsToSearch[i];
    const versionResult = await searchFilesInVersion({
      version: versionInfo,
      string,
      option,
    }, limit - result.resultCount, result.resultCount + 1);
    if (versionResult.resultCount) {
      result.resultCount += versionResult.resultCount;
      result.fileCount += versionResult.fileCount;
      result.result.push(...versionResult.result);
      filesPath.push({
        versionId: versionInfo._id,
        path: versionResult.filesPath,
      });
    }
    if (result.resultCount >= limit) {
      result.hasMoreResult = versionResult.hasMoreResult;
    }
  }
  if (filesPath.length) {
    const versionAndFilePathtoId = new Map();
    await Promise.all(filesPath.map((versionsFile) => new Promise(
      (resolve, reject) => {
        const versionString = versionsFile.versionId.toString();
        if (!versionAndFilePathtoId.has(versionString)) {
          versionAndFilePathtoId.set(versionString, new Map());
        }
        return File.find({
          versionId: versionsFile.versionId,
          path: { $in: versionsFile.path },
        },
        ['_id', 'path'])
          .then((files) => {
            files.forEach((fileData) => {
              versionAndFilePathtoId.get(versionString).set(fileData.path, fileData._id);
            });
          })
          .then(resolve)
          .catch(reject);
      }
    )));
    for (let i = 0; i < result.result.length; i += 1) {
      const data = result.result[i];
      const currentFileId = versionAndFilePathtoId.get(data.versionId.toString()).get(data.filePath);
      data.id = currentFileId;
      data.fileId = currentFileId;
      for (let i = 0; i < data.match.length; i += 1) {
        data.match[i].fileId = currentFileId;
      }
    }
  }
  return result;
}
function getSearchSuffixes(version) {
  const result = new Set();
  const suffixes = [];
  if (version.languages) {
    Object.values(version.languages).forEach((lang) => {
      suffixes.push(...(lang.suffixes || []));
    });
  }
  if (version.headers) {
    suffixes.push(...(version.headers.suffixes || []));
  }
  if (suffixes && suffixes.length) {
    suffixes.forEach((data) => {
      result.add('*'.concat(data));
    });
  }
  return Array.from(result);
}

async function searchFilesInVersion(conditions, limit, startIndex) {
  const searchResult = {};
  searchResult.fileCount = 0;
  searchResult.resultCount = 0;
  searchResult.result = [];
  searchResult.hasMoreResult = false;
  let resultString = '';
  searchResult.filesPath = [];
  let currentIndex = startIndex;
  try {
    resultString = await gitClient.searchFile(conditions);
  } catch (err) {
    throw new PhoenixError(`git grep error: ${err}`);
  }
  const resultsFileGroup = resultString.split('\n\n').filter((data) => data);
  for (let i = 0; i < resultsFileGroup.length; i += 1) {
    if (searchResult.resultCount >= limit) {
      searchResult.hasMoreResult = true;
      break;
    }
    const fileGroup = resultsFileGroup[i].split('\n');
    /**
     * split string pattern like:
     * fileName
     * line:content
     * line:content
     */
    const splitedContentLines = [];
    const fileName = './'.concat(fileGroup[0].slice(fileGroup[0].indexOf(':') + 1));
    for (let i = 1; i < fileGroup.length; i += 1) {
      const split = fileGroup[i].indexOf(':');
      splitedContentLines.push([fileGroup[i].slice(0, split), fileGroup[i].slice(split + 1)]);
    }
    // add filepath
    searchResult.result.push({
      filePath: fileName,
      versionId: conditions.version._id,
      match: [],
    });
    searchResult.filesPath.push(fileName);
    searchResult.fileCount += 1;
    const currentFile = searchResult.result[searchResult.result.length - 1];
    const match = [];
    // match case && match whole word
    let regString = conditions.string;
    if (conditions.option && conditions.option.matchWholeWord) {
      regString = `(?:^|(?<![\\w_]))(${conditions.string})(?:$|(?![\\w_]))`;
    }
    let falgs = 'g';
    if (conditions.option && conditions.option.ignoreCase) {
      falgs += 'i';
    }
    // find all matched positions in line
    for (let i = 0; i < splitedContentLines.length; i += 1) {
      const line = splitedContentLines[i][1];
      const regex = new RegExp(regString, falgs);
      let matched = regex.exec(line);
      while (matched !== null) {
        if (searchResult.resultCount < limit) {
          match.push({
            line: parseInt(splitedContentLines[i][0], 10),
            col: matched.index,
            content: line,
            id: currentIndex,
          });
          searchResult.resultCount += 1;
          currentIndex += 1;
        } else {
          searchResult.hasMoreResult = true;
          break;
        }
        matched = regex.exec(line);
      }
      if (searchResult.hasMoreResult) break;
    }
    currentFile.match.push(...match);
  }
  return searchResult;
}

async function read502FileComment(fileId) {
  const fileStream = await getFileContentStream(fileId, null, true);
  if (!fileStream) {
    return [];
  }
  const comments = [];

  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });

  let lineNumber = 0;
  let inBlock = false;
  let currentComment = [];
  // eslint-disable-next-line no-restricted-syntax
  for await (const line of rl) {
    lineNumber += 1;
    if (!inBlock) {
      const startIdx = line.indexOf('/*');
      if (startIdx !== -1) {
        const endIdx = line.indexOf('*/', startIdx + 2);
        if (endIdx !== -1) {
          // 单行注释
          comments.push({
            line: lineNumber + 1,
            comment: line.slice(startIdx, endIdx + 2).replace(/^\/\*+/, '') // 去除开始的 /*
              .replace(/\*+\/$/, '') // 去除结尾的 */
              .trim(),
          });
        } else {
          // 多行注释开始
          inBlock = true;
          currentComment.push(line.slice(startIdx).replace(/^\/\*+/, '').trim());
        }
      } else if (line.indexOf('//') !== -1) {
        comments.push({
          line: lineNumber + 1,
          comment: line.slice(line.indexOf('//') + 2).trim(),
        });
      }
    } else {
      const endIdx = line.indexOf('*/');
      if (endIdx !== -1) {
        currentComment.push(line.slice(0, endIdx + 2).replace(/\*+\/$/, '').trim());
        comments.push({
          line: lineNumber + 1,
          comment: currentComment.filter((i) => i).join('\n'),
        });
        inBlock = false;
        currentComment = [];
      } else {
        currentComment.push(line.replace(/^( )?\*+/g, '').trim());
      }
    }
  }
  return comments;
}

eventHub.on(eventKind.EVENT_VERSION_REMOVE, async (versionId) => {
  versionId = mongoose.Types.ObjectId(versionId);
  await eventHub.emit(eventKind.EVENT_FILE_REMOVE, { versionId });
  return File.deleteMany({ versionId });
});

eventHub.on([eventKind.EVENT_FILE_REMOVE, eventKind.EVENT_FILE_RESET], async ({ fileIds, versionId }) => {
  const filter = { versionId: mongoose.Types.ObjectId(versionId) };
  if (fileIds) {
    filter.fileId = { $in: [].concat(fileIds).map((id) => mongoose.Types.ObjectId(id)) };
  }
  return Promise.all([
    FileDiag.deleteMany(filter),
    FileGlobalVariable.deleteMany(filter),
    FileLiteral.deleteMany(filter),
    FileTypeSystem.deleteMany(filter),
    FileMacro.deleteMany(filter),
    DataFlow.deleteMany(filter),
    TestbedFlow.deleteMany(filter),
    FileComment.deleteMany(filter),
  ]);
});

/**
 * Export model definition object.
 */
module.exports = {
  fileCommentType,
  statusKind: fileStatusKind,
  shieldFilesStatus,
  getFileById,
  kind: fileKind,
  repositoryType,
  VERSIONFIELNAME,
  aggregateFileByfunctionIds,
  getSummaryJson,
  getJson,
  getTypesJson,

  getFile,
  shieldFiles,
  getContentGroupByFile,
  getFileContent,
  getFileByPath,
  getGroupCount,
  getFiles,
  getFilesByIds,
  getChildren,
  getTree,
  getTreeV2,
  getDiag,
  getGlobalVariables,
  getLiterals,
  getMacros,
  getTypes,
  getCompilingFunction,
  getSourceFileByVersionId,
  getFileTypeSystemByFile,
  getFileTypeSystemByVersion,

  addDirectory,
  addFile2Directory,
  addFiles,

  mergeSpecGlobalVariables,
  mergeSpecLiterals,
  mergeSpecTypeSystem,
  removeSpecGlobalVariables,
  removeSpecLiterals,
  removeSpecTypeSystem,

  removeFile,
  removeResultOfCompilingInVersion,
  removeResultOfExecutionInVersion,
  readFileComment,
  read502FileComment,

  updateFile,
  updateByUploadFile,
  updateByRemoteFile,
  updateByRemoteSource,
  updateVersionFiles,
  updateFileIncludes,
  updateVersionFilesStatus,
  updateFileStatus,
  updateEncoding,
  updateEncodingByVersionId,

  replaceGlobalVariables,
  replaceLiterals,
  replaceTypeSystem,
  replaceDiag,
  replaceMacro,
  resetFileOrDirectory,

  getOverview,
  fileCountOverview,
  computeNumberOfLines,
  computeFileCodeLines,

  getLinesOfContent,
  getFileContentStream,
  getLinesOfRemoteFileStream,

  getNormalizePath,
  cloneFilesFromMasterVersion,

  readLocalFile,

  generateOverviewXlsx,

  getLinesOfContentBatch,

  importEnvironmentFiles,
  deleteFileOriginalTypeSystem,

  searchFiles,

  getTypeNames,

};
exports = Object.assign(exports, module.exports);
